# Billing
Source: https://loops.so/docs/account/billing

How Loops billing works, including plan tiers, contact limits, and how to manage your subscription.

## How billing works

Loops billing is based on the number of contacts stored in your account. A contact is an email address that you send marketing emails to. Transactional recipients do not count as contacts (unless they are also sent marketing emails).

Your current billing plan is displayed in Stripe, which can be accessed by going to your [Billing page](https://app.loops.so/settings?page=billing) and clicking **Start/Manage subscription**.

### How we calculate your tier

Your plan tier is always calculated based on the number of contacts you have **at the time of renewal**. If your contact list has increased above or decreased below your current plan limit, you will be notified via email and moved to the appropriate tier.

Billing works as a monthly subscription paid in advance. If your contact count moves you into a higher plan tier, your service will never be interrupted. You'll simply be billed at the new rate when your next billing date arrives.

## Free plan

The free plan is available to anyone with fewer than 1,000 contacts. On the free plan:

* You can send up to 4,000 emails in any rolling 30 day window (marketing and transactional combined).
* Loops branding is placed at the bottom of your emails.
* You are limited to joining a maximum of 3 free teams. To join more, upgrade one of your teams to a paid plan, or [leave](/docs/account/team-members) or delete a team to free up space.

Once you exceed 1,000 contacts, you'll be prompted to upgrade to a paid plan. You can also upgrade at any time by clicking **Start subscription** at the top of your [Billing page](https://app.loops.so/settings?page=billing).

<Note>
  It can take up to 24 hours for contact additions or removals to be reflected in your account total.
</Note>

## Paid plans

Paid plans are tiered by contact count. Unlike the free plan, paid plans have no limit on the number of emails you can send.

Paid plan pricing is available on the [Loops pricing page](https://loops.so/pricing).

## Sending rate limits

Free accounts can send up to 10 emails per second. Paid teams can send up to 1,000 emails per second. Emails over the limit stay queued until they can be sent.

## Billing receipt emails

You can choose which email addresses receive receipts and invoices from Loops.

To add, remove or change these emails, go to your [Billing page](https://app.loops.so/settings?page=billing) and enter or remove addresses in the **Billing receipt emails** section.

## Canceling your subscription

You can cancel your subscription at any time by going to your [Billing page](https://app.loops.so/settings?page=billing) and clicking **Manage subscription**.

For further questions about billing, go to your [Support page](https://app.loops.so/settings?page=support), or contact [help@loops.so](mailto:help@loops.so).


# Change your login email
Source: https://loops.so/docs/account/changing-your-email

How to update the email address you use to log in to Loops.

You will need the account owner role to change your email address.

1. Ensure you have access to your new email address.
2. Go to [Team settings](https://app.loops.so/settings?page=team).
3. Invite your new email address to your team.
4. Accept the invite from an incognito window or a different browser.
5. Once the new email address has joined the team, promote it to an owner role.
6. Remove your old email address from the team.


# Delete your account
Source: https://loops.so/docs/account/delete-account

How to permanently delete your Loops account.

You will need the account owner role to delete your account.

<Warning>
  Deleting your account is permanent. All emails, contacts, audience data and team settings will be removed and cannot be recovered.
</Warning>

1. Go to [Account settings](https://app.loops.so/settings?page=account).
2. Scroll to the **Danger Zone**.
3. Click **Delete this team...**.
4. In the confirmation modal, review the list of sending domains that will be deleted and the actions that will happen immediately.
5. Type **permanently delete team** in the text field to confirm.
6. Click **Permanently delete**.

If you have an active paid subscription, cancel it from the [Billing settings](https://app.loops.so/settings?page=billing) before deleting your account.


# Notifications
Source: https://loops.so/docs/account/notifications

Learn how to receive notifications about new Loops contacts in external tools.

You will receive a notification when a contact is added to your audience through the API, integrations or form submission. You will not receive notifications for manually added users via CSV or other methods.

You can manage your notifications in the [Notifications](https://app.loops.so/settings?page=notifications) page.

## Email notifications

By default, you will receive an email notification from Loopsbot when a contact is added to your audience. You can disable this notification by toggling the **Notifications enabled** switch in the [Notifications section](https://app.loops.so/settings?page=notifications) of your Settings. If you'd like to receive email notifications again, you can toggle the switch back on. This setting is individual to your user account, and won't propagate to the rest of your team.

## Slack notifications

You can receive notifications in Slack for new contacts. Please note, this is a global setting and will apply to all additional seats in your account.

### Set up Slack notifications

1. Go to [Settings -> Notifications](https://app.loops.so/settings?page=notifications).
2. Click the link to be redirected to Slack for authorization.
3. Select the channel you want to add the bot to.

<Tip>
  The bot must be invited to private channels before they become visible in the dropdown.
</Tip>

4. Send a test message to verify the integration works.

That's it! All new contacts added to your audience will now send a notification to the selected Slack channel.

### Other options

Here are a few other ways to get Slack notifications for new contacts:

1. Install an email add-on to send individual emails from [Gmail](https://slack.com/apps/AEFLFJR9Q-slack-for-gmail) or [Outlook](https://slack.com/apps/AFS3736H3-slack-for-outlook) to channels or DMs (free).
2. Create a forwarding address to send individual emails to your DM with Slackbot (free).

[View the full documentation from Slack](https://slack.com/help/articles/206819278-Send-emails-to-Slack) for more information.


# Team members
Source: https://loops.so/docs/account/team-members

How to add and manage team members in your Loops account.

<Tip>It is free to add additional team members to your account.</Tip>

Here's how to add team members to your team:

1. Go to [Settings -> Team](https://app.loops.so/settings?page=team).
2. Enter the new team member's name and email address and click **Invite**.

## Owner role

You can promote members to become an Owner of your team.

Owners have special permissions:

* Owners can invite and remove team members.
* Owners can promote members to become owners.

## Managing members

You can add unlimited members to each team, for free.

Only owners can invite new members. Owners can also remove members and other owners. If you remove someone from your team, they will be established with a new Loops team of their own.


# Team switcher
Source: https://loops.so/docs/account/team-switcher

Learn how to create and connect multiple Loops accounts with one email address.

You may want to have multiple Loops accounts for different products, domains or teams.

You can create new teams from a single Loops account.

You can also combine existing Loops teams under a single email address so you can easily switch between them instead of having to log out every time.

<img alt="Team switcher" />

The existing [team member features](/docs/account/team-members) allow you to invite and remove users to and from individual teams.

<Info>
  Free users can be in a maximum of 3 teams. As soon as a user joins a paid team, they can be added to unlimited teams.
</Info>

## Create a new team

If you want to create a new team, click on the team switcher in the top left of your Loops account, then select **+ New team**.

This will create a new team and ask you to set up a new sending domain.

## Connect existing accounts

If you have existing accounts that you want to merge under one email address, there are a few steps to complete.

In this example **Team A** is the main account you want to log in with, and **Team B** is the account you want to connect.

<Steps>
  <Step title="Sign in to Team A">
    Make sure you're signed in to the Loops account you want to be your only account (Team A).
  </Step>

  <Step title="Sign in to Team B">
    **In a different browser** (or incognito window) log in to Team B (the account you want to connect) and invite your selected Team A email address from the [Team settings page](https://app.loops.so/settings?page=team).
  </Step>

  <Step title="Accept the invite">
    An invitation email will be sent to your Team A email address. Since you are already logged in to Team A, you can ignore the email.

    Refresh the Team settings page and you will see an invite alert appear in the bottom right corner. Click **Accept**.

    <img alt="Invitation popover" />

    Accepting the invite will automatically switch you to the Team A account.
  </Step>

  <Step title="Assign team ownership (optional)">
    If you want to become the [owner](/docs/account/team-members#owner-role) of Team B (giving you full permissions), go back to Team B in the other browser window and refresh the Team page. Give the **Owner** role to your Team A email address by clicking the `•••` menu button.

    Becoming owner is not required but is necessary if you are the only user in the team.
  </Step>

  <Step title="Sign out of Team B">
    You can sign out of Team B (or close your incognito window), because you can now access it through your Team A account.
  </Step>

  <Step title="Remove the old email address">
    If you want, you can safely remove the previous email address you used to sign in to Team B from the [Team settings page](https://app.loops.so/settings?page=team).
  </Step>
</Steps>

## Invite members to teams

If you want your team members to access multiple teams, simply invite the same email address to different teams. Once they accept the invitation, they will be able to switch between the teams they have access to.

## Billing within teams

Billing is handled individually per team. When you switch between teams you will be able to handle billing separately for each.


# CSV Upload
Source: https://loops.so/docs/add-users/csv-upload

Easily add contacts to Loops by uploading a CSV file.

<Tip>
  Please read through the [contact properties](/docs/contacts/properties) information to understand how to format data in your CSV file.
</Tip>

## Add new contacts via CSV

1. Select **Import** in the top right of the [Audience](https://app.loops.so/audience) page.

   <img alt="Audience page showing the Import contacts button" />

2. Hover on **CSV** and click **Upload CSV**. Download the [example formatted CSV](https://app.loops.so/loops_sample.csv) to get an idea of the columns we use. By default, we recommend using at least `Email`, `First Name`, `Last Name`, `User Group` and `Source` columns.

   <img alt="Import contacts menu showing CSV upload option" />

3. After uploading the CSV you'll have a chance to review any duplicates or missing information before finishing.

4. When the upload is finished, all the uploaded contacts can be viewed in the Audience page. That's it! 🎊

### Some important notes

* All new contacts uploaded will be automatically marked as subscribed. If you want to mark imported contacts as unsubscribed add a `Subscribed` column into your CSV file and use "false" as the column value.
* The default `Source` value for each uploaded contact will be "CSV Upload". You can change this by adding a `Source` column to your CSV file and specifying a custom value.
* Dates in your CSV file must be in one of the accepted formats listed on the [contact properties](/docs/contacts/properties#dates) page (timestamps are not supported).

## Update contacts via CSV

You can also upload a CSV file to update your existing contacts in bulk.

You can either download a CSV from your Audience page in Loops, edit the data and re-upload it, or start with a new CSV file and just include the contacts and columns you want to update.

When updating contacts, Loops will first look for a matching contact using the value in the `User ID` column, then the `Email` column.

If a contact is found, Loops will update the contact using the data provided in the CSV file. If a contact is not found with either `User ID` or `Email` values, a new contact will be created using the data provided.

### Some important notes

* The importer does not re-subscribe contacts who are marked as unsubscribed in your Audience, so updating the `Subscribed` column to `true` will not re-subscribe the contact.
* Cells that are empty in the CSV file will not overwrite existing data in Loops. If you want to clear a field, you will need to provide a value of `null` in the CSV file (4 characters, all lowercase). Note, this only works for `string` type fields.

## Trigger workflows via CSV

You can use CSV uploads as a way to trigger workflows on a group of new or existing contacts.

If you want to trigger workflows for existing contacts, download a CSV from your [Audience page](https://app.loops.so/audience) then re-upload the file. Only include the contacts you want to trigger workflows for in the CSV file.

After uploading the CSV, on the Review page select the **Trigger workflows** option. This will trigger all applicable workflows you have set up with **Contact created** and **Contact updated** triggers.

If you select to add contacts to mailing lists on this same page as well as toggle **Trigger workflows** ON, you will also trigger **Contact added to list** workflows.

<img alt="CSV upload review step showing “Trigger workflows”" />

## View previous CSV uploads

After uploading CSV files you can view a history of your uploads plus details for each file's rows.

On the [Audience page](https://app.loops.so/audience) click **Import contacts**, hover on **CSV** and click **Upload CSV**. On the next page click on **View imports**, which will show you a list of all of your past uploads.

Clicking on one of your uploads will let you view lists of all new, updated or duplicated contacts plus a list of any errors from the import.

<img alt="CSV imports history page in Loops" />


# Add contacts via integrations
Source: https://loops.so/docs/add-users/integrations

Connect Loops to external platforms to automatically add contacts to your audience.

We have a range of integrations with other platforms, which allow syncing of data between services you use.

For example, you can add users from thousands of apps through [Segment](/docs/integrations/segment) or [Zapier](/docs/integrations/zapier), or use forms on platforms like [Framer](/docs/integrations/framer) or [Webflow](/docs/integrations/webflow) to let contacts sign up to your mailing list.

[Go to the Integrations page](/docs/integrations) to view the full list.


# Add and update contacts with the Loops API
Source: https://loops.so/docs/add-users/loops-api

Loops provides a REST API to manage your contacts.

With [the Loops API](/docs/api-reference/intro), you can easily manage contacts directly from your application or service.

For example, creating a new contact is as easy as sending a `POST` request to `https://app.loops.so/api/v1/contacts/create`.

```json theme={"dark"}
{
  "email": "test@example.com",
  "firstName": "Adam",
  "lastName": "Kaczmarek",
  "favoriteColor": "blue",
  "userGroup": "Founders",
  "source": "Signup form Service"
}
```

We also offer endpoints for finding, updating and deleting contacts (plus some other features like sending transactional email and sending events).

## Learn more

<CardGroup>
  <Card title="API Reference" icon="rectangle-terminal" href="/api-reference/intro">
    Find out how to send events using our API.
  </Card>
</CardGroup>


# Add a branch
Source: https://loops.so/docs/api-reference/add-workflow-branch

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}/nodes/{nodeId}/add-branch
Add a branch and a child node under an existing Branch or Experiment node.



# API key
Source: https://loops.so/docs/api-reference/api-key

https://app.loops.so/openapi.json get /v1/api-key
Test your API key and get team context.



# Change workflow mailing list
Source: https://loops.so/docs/api-reference/change-workflow-mailing-list

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}/mailing-list
Change a workflow's mailing list.



# API Changelog
Source: https://loops.so/docs/api-reference/changelog

Release notes for the Loops REST API and webhooks, covering new endpoints, fields, breaking changes, and deprecations across OpenAPI versions.

<Update label="July 23, 2026">
  New:

  * [Create audience segment](/docs/api-reference/create-audience-segment) endpoint.
  * Endpoints for creating and editing [workflows](/docs/api-reference/intro#workflows) and [workflow nodes](/docs/api-reference/intro#workflow-nodes).
  * Endpoints for reading [event patterns](/docs/api-reference/intro#event-patterns).

  Updated: removed `queuedContactLimitReached` workflow node delete and mailing list change responses.
</Update>

<Update label="July 16, 2026">
  Updated: `rootNodeId` is now always a non-null string in [workflow](/docs/api-reference/get-workflow) responses.
</Update>

<Update label="July 8, 2026">
  New: endpoints for creating and updating themes and components.
</Update>

<Update label="July 2, 2026">
  New: [Guardian](/docs/api-reference/run-guardian-checks) endpoint for validating email message content via the API.
</Update>

<Update label="July 1, 2026">
  New: a `transactionalName` field in the [transactional.email.sent](/docs/webhooks#transactional-email-sent) webhook event.
</Update>

<Update label="June 22, 2026">
  New: endpoints for viewing [workflows](/docs/api-reference/list-workflows) and [workflow nodes](/docs/api-reference/get-workflow-node). These endpoints are currently in open alpha.
</Update>

<Update label="June 19, 2026">
  New: endpoints for [campaign groups](/docs/api-reference/list-campaign-groups), [transactional groups](/docs/api-reference/list-transactional-groups), [audience segments](/docs/api-reference/list-audience-segments), and [email message previews](/docs/api-reference/preview-email-message).

  Updated:

  * [Campaign](/docs/api-reference/create-campaign) endpoints now support audience targeting (mailing list, segment, or filter), groups, and scheduling.
  * [Email message](/docs/api-reference/update-email-message) endpoints now support CC/BCC, language, email format, and property fallbacks.
  * [Transactional email](/docs/api-reference/create-transactional-email) endpoints now support groups.
</Update>

<Update label="June 5, 2026">
  New: endpoints for creating and editing transactional emails. These endpoints are currently in an open alpha.
</Update>

<Update label="May 20, 2026">
  New: [Upload](/docs/api-reference/create-upload) endpoints for uploading images to use in emails.
</Update>

<Update label="April 24, 2026">
  New: endpoints for creating and editing campaigns. These endpoints are currently in an open alpha.
</Update>

<Update label="March 25, 2026">
  New: API endpoints to [check](/docs/api-reference/check-contact-suppression) and [remove](/docs/api-reference/remove-contact-suppression) suppression for contacts.
</Update>

<Update label="February 13, 2026">
  New: a `campaignName` field in the [campaign.email.sent](/docs/webhooks#campaign-email-sent) webhook event.
</Update>

<Update label="October 17, 2025">
  New: an [`email.resubscribed`](/docs/webhooks#email-resubscribed) webhook event for
  when contacts resubscribe to your marketing email from an email's
  "Unsubscribe" link.
</Update>

<Update label="October 13, 2025">
  New: an `optInStatus` field from [double opt-in](/docs/contacts/double-opt-in) in
  the [Find contact](/docs/api-reference/find-contact) endpoint and in
  `contact.created` [webhook events](/docs/webhooks).
</Update>

<Update label="September 29, 2025">
  New: [webhooks](/docs/webhooks) are now available to all users.
</Update>

<Update label="September 11, 2025">
  Improvement: all endpoints now return a `success` and `message` field in the error response bodies.

  `error` and `path` top-level keys are now deprecated but will continue to be supported for some time for backward compatibility.
</Update>

<Update label="August 21, 2025">
  The docs have been updated to reflect that the `email` parameter is optional
  in the [Update contact](/docs/api-reference/update-contact) endpoint if a `userId`
  is provided.
</Update>

<Update label="May 23, 2025">
  Improvement: data variables are now optional in the [Send transactional
  email](/docs/api-reference/send-transactional-email) endpoint. [Read
  more](/docs/transactional#optional-data-variables)
</Update>

<Update label="April 29, 2025">
  New: an `Idempotency-Key` header when [sending
  events](/docs/api-reference/send-event).
</Update>

<Update label="March 28, 2025">
  New: an `Idempotency-Key` header when [sending transactional
  emails](/docs/api-reference/send-transactional-email).
</Update>

<Update label="February 28, 2025">
  New: an endpoint for [listing transactional
  emails](/docs/api-reference/list-transactional-emails) and their data variables.
</Update>

<Update label="February 5, 2025">
  New: [webhooks](/docs/webhooks) are in Beta.
</Update>

<Update label="January 14, 2025">
  New: new endpoints for [creating contact properties](/docs/api-reference/create-contact-property) and [listing contact properties](/docs/api-reference/list-contact-properties).

  Deprecation: the [List custom fields](/docs/api-reference/list-custom-fields) endpoint is now deprecated in favor of the new [List contact properties](/docs/api-reference/list-contact-properties) endpoint.
</Update>

<Update label="December 18, 2024">
  New: we added `description` to mailing list objects in the [List mailing
  lists](/docs/api-reference/list-mailing-lists) endpoint.
</Update>

<Update label="November 5, 2024">
  Improvement: the maximum payload size for the transactional endpoint has been
  increased from 1MB to 4MB, allowing for more or larger attachments.
</Update>

<Update label="September 6, 2024">
  New: our new [Nuxt module](/docs/sdks/nuxt) is now out!
</Update>

<Update label="August 16, 2024">
  New: our new [Ruby SDK](/docs/sdks/ruby) is available!
</Update>

<Update label="August 15, 2024">
  New: we added an `isPublic` attribute to mailing list objects in the [List
  mailing lists](/docs/api-reference/list-mailing-lists) endpoint.
</Update>

<Update label="June 28, 2024">
  New: there's a new `addToAudience` parameter in the [Send transactional
  email](/docs/api-reference/send-transactional-email) endpoint that can add contacts to
  your audience.
</Update>

<Update label="June 18, 2024">
  New: support for our new [mailing lists](/docs/contacts/mailing-lists) feature.

  You can now add contacts to and remove contacts from mailing lists in the [Create contact](/docs/api-reference/create-contact), [Update contact](/docs/api-reference/update-contact) and [Send event](/docs/api-reference/send-event) endpoints.

  There is also a [new endpoint](/docs/api-reference/list-mailing-lists) for retrieving your mailing lists.
</Update>

<Update label="April 18, 2024">
  New: you can now [Find contacts](/docs/api-reference/find-contact) by `userId`.
</Update>

<Update label="April 9, 2024">
  New: a new endpoint for testing your integration and/or API key: [API
  key](/docs/api-reference/api-key).
</Update>

<Update label="March 21, 2024">
  New: you can now include [event properties](/docs/events/properties) in requests to
  the [Send event](/docs/api-reference/send-event) endpoint.
</Update>

<Update label="March 13, 2024">
  Improvement: we removed behavior that returned a `400 Bad Request` response if
  an unrecognized property was added to Contact endpoints.
</Update>

<Update label="February 9, 2024">
  Improvement: sending in a payload that contains an unrecognized property will
  now return a `400 Bad Request` response.
</Update>

<Update label="January 25, 2024">
  Clarification in the API docs that `userId` can be used in a [Send
  event](/docs/api-reference/send-event) request.
</Update>

<Update label="November 6, 2023">
  New: we added a new endpoint for [querying custom contact
  properties](/docs/api-reference/list-custom-fields).
</Update>

<Update label="October 30, 2023">
  Our [official JavaScript/TypeScript SDK](/docs/sdks/javascript) is now available!
</Update>

<Update label="October 12, 2023">
  New: we now accept dates as a custom [contact property](/docs/contacts/properties)
  type. [View the available formats](/docs/contacts/properties#dates).
</Update>


# Check contact suppression status
Source: https://loops.so/docs/api-reference/check-contact-suppression

https://app.loops.so/openapi.json get /v1/contacts/suppression
Check if a contact is suppressed by email address or user ID.



# Complete an upload
Source: https://loops.so/docs/api-reference/complete-upload

https://app.loops.so/openapi.json post /v1/uploads/{emailAssetId}/complete
Finalize an image upload after the file has been uploaded to the pre-signed URL.



# Create an audience segment
Source: https://loops.so/docs/api-reference/create-audience-segment

https://app.loops.so/openapi.json post /v1/audience-segments
Create a new audience segment.



# Create a campaign
Source: https://loops.so/docs/api-reference/create-campaign

https://app.loops.so/openapi.json post /v1/campaigns
Create a draft marketing campaign and its associated email message.



# Create a campaign group
Source: https://loops.so/docs/api-reference/create-campaign-group

https://app.loops.so/openapi.json post /v1/campaign-groups
Create a new campaign group.



# Create a component
Source: https://loops.so/docs/api-reference/create-component

https://app.loops.so/openapi.json post /v1/components
Create a new email component from an LMX body.



# Create a contact
Source: https://loops.so/docs/api-reference/create-contact

https://app.loops.so/openapi.json post /v1/contacts/create
Add a contact to your audience.



# Create a contact property
Source: https://loops.so/docs/api-reference/create-contact-property

https://app.loops.so/openapi.json post /v1/contacts/properties
Create a custom contact property.



# Create a theme
Source: https://loops.so/docs/api-reference/create-theme

https://app.loops.so/openapi.json post /v1/themes
Create a new email theme for use across Loops emails.



# Create a transactional email
Source: https://loops.so/docs/api-reference/create-transactional-email

https://app.loops.so/openapi.json post /v1/transactional-emails
Create a new transactional email and its draft email message, ready for editing, previewing, and publishing through the Loops API.



# Create a transactional group
Source: https://loops.so/docs/api-reference/create-transactional-group

https://app.loops.so/openapi.json post /v1/transactional-groups
Create a new transactional group.



# Create an upload
Source: https://loops.so/docs/api-reference/create-upload

https://app.loops.so/openapi.json post /v1/uploads
Request a pre-signed URL to upload an image asset.



# Create a workflow
Source: https://loops.so/docs/api-reference/create-workflow

https://app.loops.so/openapi.json post /v1/workflows
Create a draft workflow with a blank trigger and exit node.



# Create a workflow node
Source: https://loops.so/docs/api-reference/create-workflow-node

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}/nodes
Create a new default workflow node and return it with the latest workflow



# List dedicated sending IP addresses
Source: https://loops.so/docs/api-reference/dedicated-sending-ips

https://app.loops.so/openapi.json get /v1/dedicated-sending-ips
Retrieve a list of Loops' dedicated sending IP addresses.



# Delete a contact
Source: https://loops.so/docs/api-reference/delete-contact

https://app.loops.so/openapi.json post /v1/contacts/delete
Delete a contact by email address or user ID.



# Delete a workflow node
Source: https://loops.so/docs/api-reference/delete-workflow-node

https://app.loops.so/openapi.json delete /v1/workflows/{workflowId}/nodes/{nodeId}
Delete a single workflow node.



# Delete workflow nodes recursively
Source: https://loops.so/docs/api-reference/delete-workflow-nodes

https://app.loops.so/openapi.json delete /v1/workflows/{workflowId}/nodes/{nodeId}/recursive
Delete a node and its downstream subtree.



# Ensure a transactional email has a draft
Source: https://loops.so/docs/api-reference/ensure-transactional-draft

https://app.loops.so/openapi.json post /v1/transactional-emails/{transactionalId}/draft
Ensure a transactional email has a draft email message ready for editing, creating one from the published version if no draft currently exists.



# API Examples
Source: https://loops.so/docs/api-reference/examples

Browse copy/paste code examples for the Loops API and SDKs covering contacts, events, campaigns, transactional emails, and content revisions.

<CardGroup>
  <Card title="Contacts" href="/api-reference/examples/contacts" icon="user">
    Code examples for managing contacts.
  </Card>

  <Card title="Campaigns" href="/api-reference/examples/campaigns" icon="megaphone">
    Code examples for creating campaigns, targeting audiences, sending previews, and editing email content with LMX.
  </Card>

  <Card title="Workflows" href="/api-reference/examples/workflows" icon="arrows-rotate">
    Code examples for accessing workflows and their nodes.
  </Card>

  <Card title="Events" href="/api-reference/examples/events" icon="bolt">
    Code examples for sending events.
  </Card>

  <Card title="Transactional emails" href="/api-reference/examples/transactional-emails" icon="rectangle-terminal">
    Code examples for creating, previewing, publishing, and sending transactional emails.
  </Card>
</CardGroup>


# Campaigns API examples
Source: https://loops.so/docs/api-reference/examples/campaigns

Code examples for creating campaigns, targeting audiences, sending previews, and updating email messages with content revisions via the Loops API, SDKs, and CLI.

## Create a campaign

This creates a draft campaign and a related email message in one request.

Only a `name` value is required.

<Tip>
  Save the returned `emailMessageContentRevisionId`. Pass it as
  `expectedRevisionId` when updating an email message to avoid `409 Conflict` errors caused by stale revisions.
</Tip>

[API reference](/docs/api-reference/create-campaign)\
[CLI reference](/docs/cli/campaigns#create)

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

  ```js JavaScript theme={"dark"}
  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;
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const data = await loops.createCampaign({
    name: "Spring product announcement",
  });

  const campaignId = data.id;
  const emailMessageId = data.emailMessageId;
  const emailMessageContentRevisionId = data.emailMessageContentRevisionId;
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $result = $loops->campaigns->create(name: 'Spring product announcement');

  $campaign_id = $result['id'];
  $email_message_id = $result['emailMessageId'];
  $content_revision_id = $result['emailMessageContentRevisionId'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Campaigns.create(
    name: "Spring product announcement",
  )

  campaign_id = response["id"]
  email_message_id = response["emailMessageId"]
  content_revision_id = response["emailMessageContentRevisionId"]
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/campaigns",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "name": "Spring product announcement",
      },
  )

  data = response.json()
  campaign_id = data["id"]
  email_message_id = data["emailMessageId"]
  content_revision_id = data["emailMessageContentRevisionId"]
  ```
</CodeGroup>

## 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](/docs/api-reference/list-themes) [CLI reference](/docs/cli/themes#list)\
List components [API reference](/docs/api-reference/list-components) [CLI reference](/docs/cli/components#list)

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

  ```js JavaScript theme={"dark"}
  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();
  ```

  ```js JavaScript SDK theme={"dark"}
  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 }),
  ]);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $themes = $loops->themes->list(per_page: 20);
  $components = $loops->components->list(per_page: 20);
  ```

  ```ruby Ruby SDK theme={"dark"}
  themes = LoopsSdk::Themes.list(perPage: 20)
  components = LoopsSdk::Components.list(perPage: 20)
  ```

  ```python Python theme={"dark"}
  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()
  ```
</CodeGroup>

## Update the related email message with `contentRevisionId`

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.

<Tip>
  Save the returned `contentRevisionId` after each update. Pass it as
  `expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
  by stale revisions.
</Tip>

[API reference](/docs/api-reference/update-email-message)\
[CLI reference](/docs/cli/email-messages#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops email-messages update $emailMessageId \
    --expected-revision-id $emailMessageContentRevisionId \
    --subject "Big spring updates" \
    --preview-text "A quick look at what's new" \
    --from-name "Loops" \
    --from-email hello \
    --reply-to support@example.com \
    --lmx-file ./email.lmx
  ```

  ```js JavaScript theme={"dark"}
  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;
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  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 updated = await loops.updateEmailMessage(emailMessageId, {
    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 nextContentRevisionId = updated.contentRevisionId;
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $lmx_content = <<<LMX
  <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>
  LMX;

  $updated = $loops->emailMessages->update(
    email_message_id: $email_message_id,
    expected_revision_id: $content_revision_id,
    subject: 'Big spring updates',
    preview_text: 'A quick look at what\'s new',
    from_name: 'Loops',
    from_email: 'hello',
    reply_to_email: 'support@example.com',
    lmx: $lmx_content,
  );

  $next_content_revision_id = $updated['contentRevisionId'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  lmx_content = <<~LMX
    <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>
  LMX

  updated = LoopsSdk::EmailMessages.update(
    email_message_id: email_message_id,
    expected_revision_id: content_revision_id,
    subject: "Big spring updates",
    preview_text: "A quick look at what's new",
    from_name: "Loops",
    from_email: "hello",
    reply_to_email: "support@example.com",
    lmx: lmx_content,
  )

  next_content_revision_id = updated["contentRevisionId"]
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "expectedRevisionId": content_revision_id,
          "subject": "Big spring updates",
          "previewText": "A quick look at what's new",
          "fromName": "Loops",
          "fromEmail": "hello",
          "replyToEmail": "support@example.com",
          "lmx": "<Style backgroundColor=\"#ffffff\" textBaseColor=\"#111111\" /><Section><Paragraph>Hey there, here is what's new.</Paragraph></Section>",
      },
  )

  updated = response.json()
  next_content_revision_id = updated["contentRevisionId"]
  ```
</CodeGroup>

## 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](/docs/api-reference/create-upload)\
[Complete upload API reference](/docs/api-reference/complete-upload)\
[Upload CLI reference](/docs/cli/uploads#create)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops uploads create ./header.png -o json
  ```

  ```js JavaScript theme={"dark"}
  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();
  ```

  ```js JavaScript SDK theme={"dark"}
  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);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $result = $loops->uploads->upload(path: './header.png');

  $final_url = $result['finalUrl'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Uploads.upload(path: "./header.png")

  final_url = response["finalUrl"]
  ```

  ```python Python theme={"dark"}
  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"]
  ```
</CodeGroup>

## Target an audience segment

List your [audience segments](/docs/api-reference/list-audience-segments) to find a
segment ID, then apply it to a draft campaign with
[Update a campaign](/docs/api-reference/update-campaign).

Setting `audienceSegmentId` clears any existing `audienceFilter` on the campaign.

[API reference](/docs/api-reference/update-campaign)\
[CLI reference](/docs/cli/campaigns#update)

<CodeGroup>
  ```js JavaScript theme={"dark"}
  const segmentsResponse = await fetch(
    "https://app.loops.so/api/v1/audience-segments",
    {
      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();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

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

  const updatedCampaign = await loops.updateCampaign(campaignId, {
    audienceSegmentId,
  });
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $segments = $loops->audienceSegments->list();
  $audience_segment_id = $segments['data'][0]['id'];

  $updated_campaign = $loops->campaigns->update(
    campaign_id: $campaign_id,
    audience_segment_id: $audience_segment_id,
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  segments = LoopsSdk::AudienceSegments.list
  audience_segment_id = segments["data"][0]["id"]

  updated_campaign = LoopsSdk::Campaigns.update(
    campaign_id: campaign_id,
    audience_segment_id: audience_segment_id,
  )
  ```

  ```python Python theme={"dark"}
  import requests

  segments_response = requests.get(
      "https://app.loops.so/api/v1/audience-segments",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
  )

  segments = segments_response.json()["data"]
  audience_segment_id = segments[0]["id"]

  update_response = requests.post(
      f"https://app.loops.so/api/v1/campaigns/{campaign_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "audienceSegmentId": audience_segment_id,
      },
  )

  updated_campaign = update_response.json()
  ```
</CodeGroup>

You can also set the segment when creating the campaign:

<CodeGroup>
  ```js JavaScript theme={"dark"}
  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: "clr4c8t0v008yl70x3y4z5a6b",
    }),
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const data = await loops.createCampaign({
    name: "Spring product announcement",
    audienceSegmentId: "clr4c8t0v008yl70x3y4z5a6b",
  });
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $result = $loops->campaigns->create(
    name: 'Spring product announcement',
    audience_segment_id: 'clr4c8t0v008yl70x3y4z5a6b',
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Campaigns.create(
    name: "Spring product announcement",
    audience_segment_id: "clr4c8t0v008yl70x3y4z5a6b",
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/campaigns",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "name": "Spring product announcement",
          "audienceSegmentId": "clr4c8t0v008yl70x3y4z5a6b",
      },
  )
  ```
</CodeGroup>

## 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](/docs/api-reference/preview-email-message)

<CodeGroup>
  ```js JavaScript theme={"dark"}
  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();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

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

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $preview = $loops->emailMessages->preview(
    email_message_id: $email_message_id,
    emails: ['you@example.com'],
    contact_properties: [
      'firstName' => 'Alex',
      'plan' => 'Pro',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  preview = LoopsSdk::EmailMessages.preview(
    email_message_id: email_message_id,
    emails: ["you@example.com"],
    contact_properties: {
      firstName: "Alex",
      plan: "Pro",
    },
  )
  ```

  ```python Python theme={"dark"}
  import requests

  preview_response = requests.post(
      f"https://app.loops.so/api/v1/email-messages/{email_message_id}/preview",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "emails": ["you@example.com"],
          "contactProperties": {
              "firstName": "Alex",
              "plan": "Pro",
          },
      },
  )

  preview = preview_response.json()
  ```
</CodeGroup>


# Contacts API examples
Source: https://loops.so/docs/api-reference/examples/contacts

Copy/paste code examples for managing contacts with the Loops API, CLI, and SDKs, including create, update, find, and delete requests with sample payloads.

## Create a contact

[API reference](/docs/api-reference/create-contact)\
[CLI reference](/docs/cli/contacts#create)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts create \
    --email test@example.com \
    --first-name John \
    --last-name Doe
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/create", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email: "test@example.com",
      firstName: "John",
      lastName: "Doe",
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.createContact(
    "test@example.com",
    {
      firstName: "John",
      lastName: "Doe"
    },
  );
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->create(
    email: 'test@example.com',
    properties: [
      'firstName' => 'John',
      'lastName' => 'Doe',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Contacts.create(
    email: "test@example.com",
    properties: {
      firstName: "John",
      lastName: "Doe",
    },
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/contacts/create",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "firstName": "John",
          "lastName": "Doe"
      }
  )
  ```
</CodeGroup>

## Create a contact and add them to a mailing list

[API reference](/docs/api-reference/create-contact)\
[CLI reference](/docs/cli/contacts#create)

<CodeGroup>
  ```bash CLI {5} theme={"dark"}
  loops contacts create \
    --email test@example.com \
    --first-name John \
    --last-name Doe \
    --list "<mailing-list-id>=true"
  ```

  ```js JavaScript {11-13} theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/create", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email: "test@example.com",
      firstName: "John",
      lastName: "Doe",
      mailingLists: {
        "<mailing-list-id>": true
      },
    }),
  });

  const data = await response.json();
  ```

  ```js JavaScript SDK {10-12,17} theme={"dark"}
  import { LoopsClient } from "loops";

  const loops = new LoopsClient("<your-api-key>");

  const properties = {
    firstName: "John",
    lastName: "Doe",
  };

  const mailingLists = {
    "<mailing-list-id>": true,
  };

  const response = await loops.createContact(
    "test@example.com",
    properties,
    mailingLists,
  );
  ```

  ```php PHP SDK {11-13} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->create(
    email: 'test@example.com',
    properties: [
      'firstName' => 'John',
      'lastName' => 'Doe',
    ],
    mailing_lists: [
      '<mailing-list-id>' => TRUE,
    ],
  );
  ```

  ```ruby Ruby SDK {7-9} theme={"dark"}
  response = LoopsSdk::Contacts.create(
    email: "test@example.com",
    properties: {
      firstName: "John",
      lastName: "Doe",
    },
    mailing_lists: {
      "<mailing-list-id>": true,
    },
  )
  ```

  ```python Python {13-15} theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/contacts/create",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "firstName": "John",
          "lastName": "Doe",
          "mailingLists": {
              "<mailing-list-id>": True
          }
      }
  )
  ```
</CodeGroup>

## Update a contact

When updating a contact you must provide an `email` or `userId` value to identify the contact.

<Tip>
  You can use the "update" endpoint to update or create contacts. If the provided email or user ID does not exist, a new contact will be created.
</Tip>

[API reference](/docs/api-reference/update-contact)\
[CLI reference](/docs/cli/contacts#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts update \
    --email test@example.com \
    --prop planName=Pro
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      planName: "Pro",
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.updateContact(
    "test@example.com",
    {
      planName: "Pro",
    },
  );
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->update(
    email: 'test@example.com',
    properties: [
      'planName' => 'Pro',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Contacts.update(
    email: "test@example.com",
    properties: {
      planName: "Pro",
    },
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.put(
      "https://app.loops.so/api/v1/contacts/update",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "planName": "Pro"
      }
  )
  ```
</CodeGroup>

## Update a contact's email address

For this the contact will need to already have a `userId` value set.

[API reference](/docs/api-reference/update-contact)\
[CLI reference](/docs/cli/contacts#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts update \
    --user-id 12345 \
    --email test@example.com
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      userId: "12345",
      email: "test@example.com",
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.updateContact(
    "test@example.com",
    {
      userId: "12345",
    },
  );
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->update(
    email: 'test@example.com',
    properties: [
      'userId' => '12345',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Contacts.update(
    email: "test@example.com",
    properties: {
      userId: "12345",
    },
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.put(
      "https://app.loops.so/api/v1/contacts/update",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "userId": "12345"
      }
  )
  ```
</CodeGroup>

## Subscribe a contact to a mailing list

[API reference](/docs/api-reference/update-contact)\
[CLI reference](/docs/cli/contacts#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts update \
    --email test@example.com \
    --list "<mailing-list-id>=true"
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      mailingLists: {
        "<mailing-list-id>" => true
      },
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.updateContact(
    "test@example.com",
    {},
    {
      "<mailing-list-id>": true,
    },
  );
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->update(
    email: 'test@example.com',
    mailing_lists: [
      '<mailing-list-id>' => TRUE,
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Contacts.update(
    email: "test@example.com",
    mailing_lists: {
      "<mailing-list-id>" => true,
    },
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.put(
      "https://app.loops.so/api/v1/contacts/update",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "mailingLists": {
              "<mailing-list-id>": True
          }
      }
  )
  ```
</CodeGroup>

## Unsubscribe a contact from a mailing list

This removes a contact from a specific mailing list. [See below](#unsubscribe-a-contact) to see how to fully unsubscribe a contact.

Use `false` to unsubscribe a contact from a mailing list.

[API reference](/docs/api-reference/update-contact)\
[CLI reference](/docs/cli/contacts#update)

<CodeGroup>
  ```bash CLI {4} theme={"dark"}
  loops contacts update \
    --email test@example.com \
    --list "<mailing-list-id>=false"
  ```

  ```js JavaScript {9-11} theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      mailingLists: {
        "<mailing-list-id>" => false
      },
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.updateContact(
    "test@example.com",
    {},
    {
      "<mailing-list-id>" => false,
    },
  );
  ```

  ```php PHP SDK {7-9} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->update(
    email: 'test@example.com',
    mailing_lists: [
      '<mailing-list-id>' => FALSE,
    ],
  );
  ```

  ```ruby Ruby SDK {3-5} theme={"dark"}
  response = LoopsSdk::Contacts.update(
    email: "test@example.com",
    mailing_lists: {
      "<mailing-list-id>" => false,
    },
  )
  ```

  ```python Python {11-13} theme={"dark"}
  import requests

  response = requests.put(
      "https://app.loops.so/api/v1/contacts/update",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "mailingLists": {
              "<mailing-list-id>": False
          }
      }
  )
  ```
</CodeGroup>

## Unsubscribe a contact

Set `subscribed` to `false` to unsubscribe a contact. The contact will no longer receive campaign or workflow emails, but will remain listed in your audience.

[API reference](/docs/api-reference/update-contact)\
[CLI reference](/docs/cli/contacts#update)

<CodeGroup>
  ```bash CLI {3} theme={"dark"}
  loops contacts update \
    --email test@example.com \
    --subscribed false
  ```

  ```js JavaScript {9} theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      subscribed: false,
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.updateContact(
    "test@example.com",
    {
      subscribed: false,
    },
  );
  ```

  ```php PHP SDK {7} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->update(
    email: 'test@example.com',
    subscribed: false,
  );
  ```

  ```ruby Ruby SDK {3} theme={"dark"}
  response = LoopsSdk::Contacts.update(
    email: "test@example.com",
    subscribed: false,
  )
  ```

  ```python Python {11} theme={"dark"}
  import requests

  response = requests.put(
      "https://app.loops.so/api/v1/contacts/update",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "subscribed": False
      }
  )
  ```
</CodeGroup>

## Delete a contact

You can delete contacts by email or user ID.

[API reference](/docs/api-reference/delete-contact)\
[CLI reference](/docs/cli/contacts#delete)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts delete --email test@example.com
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/contacts/delete", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
    }),
  });

  const data = await response.json();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.deleteContact({
    email: "test@example.com",
  });
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->contacts->delete(
    email: 'test@example.com',
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Contacts.delete(
    email: "test@example.com",
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/contacts/delete",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
      }
  )
  ```
</CodeGroup>

## Check contact suppression status

[API reference](/docs/api-reference/check-contact-suppression)\
[CLI reference](/docs/cli/contacts#suppression-check)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts suppression check --email test@example.com
  ```

  ```js JavaScript theme={"dark"}
  const params = new URLSearchParams({ email: "test@example.com" });

  const response = await fetch(
    `https://app.loops.so/api/v1/contacts/suppression?${params.toString()}`,
    {
      method: "GET",
      headers: {
        "Authorization": "Bearer <your-api-key>",
      },
    },
  );

  const data = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.get(
      "https://app.loops.so/api/v1/contacts/suppression",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
      params={
          "email": "test@example.com",
      },
  )

  data = response.json()
  ```
</CodeGroup>

## Remove suppression for a contact

[API reference](/docs/api-reference/remove-contact-suppression)\
[CLI reference](/docs/cli/contacts#suppression-remove)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops contacts suppression remove --email test@example.com
  ```

  ```js JavaScript theme={"dark"}
  const params = new URLSearchParams({ email: "test@example.com" });

  const response = await fetch(
    `https://app.loops.so/api/v1/contacts/suppression?${params.toString()}`,
    {
      method: "DELETE",
      headers: {
        "Authorization": "Bearer <your-api-key>",
      },
    },
  );

  const data = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.delete(
      "https://app.loops.so/api/v1/contacts/suppression",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
      params={
          "email": "test@example.com",
      },
  )

  data = response.json()
  ```
</CodeGroup>


# Events API examples
Source: https://loops.so/docs/api-reference/examples/events

Copy/paste code examples for sending events to Loops via the API, CLI, and SDKs to trigger workflows and attach event properties, with sample payloads.

## Send an event

[API reference](/docs/api-reference/send-event)\
[CLI reference](/docs/cli/events#send)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops events send testEvent --email test@example.com
  ```

  ```js JavaScript theme={"dark"}
  await fetch("https://app.loops.so/api/v1/events/send", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      eventName: "testEvent",
    }),
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.sendEvent({
    email: "test@example.com",
    eventName: "testEvent",
  });
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->events->send(
    email: 'test@example.com',
    event_name: 'testEvent',
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Events.send(
    email: "test@example.com",
    event_name: "testEvent",
  )
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/events/send",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "eventName": "testEvent",
      }
  )
  ```
</CodeGroup>

## Send an event with event properties

Include data that can be used in your workflow emails triggered by the event.

[API reference](/docs/api-reference/send-event)\
[CLI reference](/docs/cli/events#send)

<CodeGroup>
  ```bash CLI {3} theme={"dark"}
  loops events send testEvent \
    --email test@example.com \
    --props ./event-props.json
  ```

  ```js JavaScript {10-12} theme={"dark"}
  await fetch("https://app.loops.so/api/v1/events/send", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      eventName: "testEvent",
      eventProperties: {
        "testProperty": "testValue",
      },
    }),
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.sendEvent({
    email: "test@example.com",
    eventName: "testEvent",
    eventProperties: {
      "testProperty": "testValue",
    },
  });
  ```

  ```php PHP SDK {8-10} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->events->send(
    email: 'test@example.com',
    event_name: 'testEvent',
    event_properties: [
      'testProperty' => 'testValue',
    ],
  );
  ```

  ```ruby Ruby SDK {4-6} theme={"dark"}
  response = LoopsSdk::Events.send(
    email: "test@example.com",
    event_name: "testEvent",
    event_properties: {
      testProperty: "testValue",
    },
  )
  ```

  ```python Python {12-14} theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/events/send",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "eventName": "testEvent",
          "eventProperties": {
              "testProperty": "testValue",
          },
      }
  )
  ```
</CodeGroup>

## Send an event and update the contact

Include contact properties to update the contact as the event is sent.

[API reference](/docs/api-reference/send-event)\
[CLI reference](/docs/cli/events#send)

<CodeGroup>
  ```bash CLI {3} theme={"dark"}
  loops events send testEvent \
    --email test@example.com \
    --contact-props ./contact-props.json
  ```

  ```js JavaScript {10} theme={"dark"}
  await fetch("https://app.loops.so/api/v1/events/send", {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "test@example.com",
      eventName: "testEvent",
      planName: "Pro", // Contact property
    }),
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.sendEvent({
    email: "test@example.com",
    eventName: "testEvent",
    contactProperties: {
      planName: "Pro",
    },
  });
  ```

  ```php PHP SDK {8-10} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->events->send(
    email: 'test@example.com',
    event_name: 'testEvent',
    contact_properties: [
      'planName' => 'Pro',
    ],
  );
  ```

  ```ruby Ruby SDK {4-6} theme={"dark"}
  response = LoopsSdk::Events.send(
    email: "test@example.com",
    event_name: "testEvent",
    contact_properties: {
      planName: "Pro",
    },
  )
  ```

  ```python Python {12-14} theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/events/send",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
      },
      json={
          "email": "test@example.com",
          "eventName": "testEvent",
          "contactProperties": {
              "planName": "Pro",
          },
      }
  )
  ```
</CodeGroup>

## Send an event with an idempotency key

Add an `Idempotency-Key` header to the request to prevent duplicate requests.

[API reference](/docs/api-reference/send-event#param-idempotency-key)\
[CLI reference](/docs/cli/events#send)

<CodeGroup>
  ```bash CLI {3} theme={"dark"}
  loops events send testEvent \
    --email test@example.com \
    --idempotency-key 550e8400-e29b-41d4-a716-446655440000
  ```

  ```js JavaScript {6} theme={"dark"}
  await fetch("https://app.loops.so/api/v1/events/send", {
    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",
      eventName: "testEvent",
    }),
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.sendEvent({
    email: "test@example.com",
    eventName: "testEvent",
    headers: {
      "Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
    },
  });
  ```

  ```php PHP SDK {8-10} theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->events->send(
    email: 'test@example.com',
    event_name: 'testEvent',
    headers: [
      'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000',
    ],
  );
  ```

  ```ruby Ruby SDK {4-6} theme={"dark"}
  response = LoopsSdk::Events.send(
    email: "test@example.com",
    event_name: "testEvent",
    headers: {
      "Idempotency-Key" => "550e8400-e29b-41d4-a716-446655440000",
    },
  )
  ```

  ```python Python {8} theme={"dark"}
  import requests

  response = requests.post(
      "https://app.loops.so/api/v1/events/send",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json"
          "Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
      },
      json={
          "email": "test@example.com",
          "eventName": "testEvent",
      }
  )
  ```
</CodeGroup>


# Transactional email API examples
Source: https://loops.so/docs/api-reference/examples/transactional-emails

Code examples for creating transactional emails, updating drafts with content revisions, previewing, publishing, and sending via the Loops API, CLI, and SDKs.

## Create a transactional email

This creates a transactional email and a related draft email message in one request.

Only a `name` value is required.

<Tip>
  Save the returned `draftEmailMessageContentRevisionId`. Pass it as
  `expectedRevisionId` when updating the draft email message to avoid `409 Conflict` errors caused by stale revisions.
</Tip>

[API reference](/docs/api-reference/create-transactional-email)

There is no CLI command for creating transactional emails yet — use the API below.

<CodeGroup>
  ```js JavaScript theme={"dark"}
  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;
  ```

  ```js JavaScript SDK theme={"dark"}
  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;
  ```

  ```php PHP SDK theme={"dark"}
  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'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  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"]
  ```

  ```python Python theme={"dark"}
  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"
  ]
  ```
</CodeGroup>

## 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](/docs/api-reference/list-themes) [CLI reference](/docs/cli/themes#list)\
List components [API reference](/docs/api-reference/list-components) [CLI reference](/docs/cli/components#list)

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

  ```js JavaScript theme={"dark"}
  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();
  ```

  ```js JavaScript SDK theme={"dark"}
  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 }),
  ]);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $themes = $loops->themes->list(per_page: 20);
  $components = $loops->components->list(per_page: 20);
  ```

  ```ruby Ruby SDK theme={"dark"}
  themes = LoopsSdk::Themes.list(perPage: 20)
  components = LoopsSdk::Components.list(perPage: 20)
  ```

  ```python Python theme={"dark"}
  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()
  ```
</CodeGroup>

## 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.

<Tip>
  Save the returned `contentRevisionId` after each update. Pass it as
  `expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
  by stale revisions.
</Tip>

[API reference](/docs/api-reference/update-email-message)\
[CLI reference](/docs/cli/email-messages#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  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
  ```

  ```js JavaScript theme={"dark"}
  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;
  ```

  ```js JavaScript SDK theme={"dark"}
  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;
  ```

  ```php PHP SDK theme={"dark"}
  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'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  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"]
  ```

  ```python Python theme={"dark"}
  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"]
  ```
</CodeGroup>

## Send a transactional preview

After updating the draft email message, send a test preview to one or more
addresses before publishing. Transactional previews accept `dataVariables` 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](/docs/api-reference/preview-email-message)

<CodeGroup>
  ```js JavaScript theme={"dark"}
  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();
  ```

  ```js JavaScript SDK theme={"dark"}
  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",
    },
  });
  ```

  ```php PHP SDK theme={"dark"}
  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',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  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",
    },
  )
  ```

  ```python Python theme={"dark"}
  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()
  ```
</CodeGroup>

## 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](/docs/api-reference/create-upload)\
[Complete upload API reference](/docs/api-reference/complete-upload)
[Upload CLI reference](/docs/cli/uploads#create)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops uploads create ./header.png -o json
  ```

  ```js JavaScript theme={"dark"}
  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();
  ```

  ```js JavaScript SDK theme={"dark"}
  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);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $result = $loops->uploads->upload(path: './header.png');

  $final_url = $result['finalUrl'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Uploads.upload(path: "./header.png")

  final_url = response["finalUrl"]
  ```

  ```python Python theme={"dark"}
  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"]
  ```
</CodeGroup>

## 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 reference](/docs/api-reference/publish-transactional-email)

<CodeGroup>
  ```js JavaScript theme={"dark"}
  const 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();
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const published = await loops.publishTransactionalEmail(transactionalId);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $published = $loops->transactional->publish(transactional_id: $transactional_id);
  ```

  ```ruby Ruby SDK theme={"dark"}
  published = LoopsSdk::Transactional.publish(
    transactional_id: transactional_id,
  )
  ```

  ```python Python theme={"dark"}
  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()
  ```
</CodeGroup>

## Send a transactional email

Use the transactional `id` from create (or publish) as `transactionalId` in the send request.

[API reference](/docs/api-reference/send-transactional-email)\
[CLI reference](/docs/cli/transactional#send)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops transactional send <transactional-id> \
    --email test@example.com \
    --var loginUrl=https://example.com/login
  ```

  ```js JavaScript theme={"dark"}
  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",
      },
    }),
  });
  ```

  ```js JavaScript SDK theme={"dark"}
  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",
    },
  });
  ```

  ```php PHP SDK theme={"dark"}
  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',
    ],
  );
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Transactional.send(
    email: "test@example.com",
    transactional_id: "<transactional-id>",
    data_variables: {
      loginUrl: "https://example.com/login",
    },
  )
  ```

  ```python Python theme={"dark"}
  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",
          },
      },
  )
  ```
</CodeGroup>

## Send a transactional email with an array data variable

Learn more about [arrays](/docs/creating-emails/editor#arrays).

[API reference](/docs/api-reference/send-transactional-email#param-data-variables)\
[CLI reference](/docs/cli/transactional#send)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops transactional send <id> \
    --email test@example.com \
    --json-vars ./vars.json
  ```

  ```js JavaScript {11-14} theme={"dark"}
  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" },
        ],
      },
    }),
  });
  ```

  ```js JavaScript SDK {9-12} theme={"dark"}
  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" },
      ],
    },
  });
  ```

  ```php PHP SDK {9-18} theme={"dark"}
  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',
        ],
      ],
    ],
  );
  ```

  ```ruby Ruby SDK {5-8} theme={"dark"}
  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" },
      ],
    },
  )
  ```

  ```python Python {13-16} theme={"dark"}
  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" },
        ],
      },
    },
  )
  ```
</CodeGroup>

## Send a transactional email with attachments

<Note>
  You must request attachments to be enabled in your account before you can send emails with them.
</Note>

[API reference](/docs/api-reference/send-transactional-email#param-attachments)\
[CLI reference](/docs/cli/transactional#send)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops transactional send <id> \
    --email test@example.com \
    --var loginUrl=https://example.com/login \
    --attachment ./example.pdf
  ```

  ```js JavaScript {13-19} theme={"dark"}
  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>",
        },
      ],
    }),
  });
  ```

  ```js JavaScript SDK {11-17} theme={"dark"}
  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>",
      },
    ],
  });
  ```

  ```php PHP SDK {11-17} theme={"dark"}
  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')),
      ],
    ],
  );
  ```

  ```ruby Ruby SDK {7-13} theme={"dark"}
  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>',
      },
    ],
  )
  ```

  ```python Python {15-21} theme={"dark"}
  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>",
              },
          ],
      },
  )
  ```
</CodeGroup>

## Send a transactional email with an idempotency key

Add an `Idempotency-Key` header to the request to prevent duplicate requests.

[API reference](/docs/api-reference/send-transactional-email#param-idempotency-key)\
[CLI reference](/docs/cli/transactional#send)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops transactional send <id> \
    --email test@example.com \
    --var loginUrl=https://example.com/login \
    --idempotency-key 550e8400-e29b-41d4-a716-446655440000
  ```

  ```js JavaScript {6} theme={"dark"}
  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",
      },
    }),
  });
  ```

  ```js JavaScript SDK {11-13} theme={"dark"}
  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",
    },
  });
  ```

  ```php PHP SDK {11-13} theme={"dark"}
  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',
    ],
  );
  ```

  ```ruby Ruby SDK {7-9} theme={"dark"}
  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',
    },
  )
  ```

  ```python Python {8} theme={"dark"}
  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",
          },
      },
  )
  ```
</CodeGroup>

## List transactional emails

[API reference](/docs/api-reference/list-transactional-emails)\
[CLI reference](/docs/cli/transactional#list)

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

  ```js JavaScript theme={"dark"}
  await fetch("https://app.loops.so/api/v1/transactional-emails", {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  });
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const response = await loops.listTransactionalEmails();
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient("<your-api-key>");

  $result = $loops->transactional->list();
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Transactional.list
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.get(
      "https://app.loops.so/api/v1/transactional-emails",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
  )
  ```
</CodeGroup>


# Workflows API examples
Source: https://loops.so/docs/api-reference/examples/workflows

Copy/paste code examples for listing workflows, retrieving workflow graphs, and fetching detailed workflow node configurations via the Loops API, SDKs, and CLI.

## List workflows

Retrieve a paginated list of workflows, most recently updated first.

[API reference](/docs/api-reference/list-workflows)\
[CLI reference](/docs/cli/workflows#list)

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

  ```js JavaScript theme={"dark"}
  const response = await fetch("https://app.loops.so/api/v1/workflows?perPage=20", {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  });

  const { data, pagination } = await response.json();
  const workflowId = data[0]?.id;
  const nextCursor = pagination.nextCursor;
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const { data, pagination } = await loops.listWorkflows({ perPage: 20 });
  const workflowId = data[0]?.id;
  const nextCursor = pagination.nextCursor;
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $result = $loops->workflows->list(per_page: 20);
  $workflow_id = $result['data'][0]['id'] ?? null;
  $next_cursor = $result['pagination']['nextCursor'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  response = LoopsSdk::Workflows.list(perPage: 20)

  workflow_id = response["data"][0]&.dig("id")
  next_cursor = response["pagination"]["nextCursor"]
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.get(
      "https://app.loops.so/api/v1/workflows",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
      params={"perPage": 20},
  )

  body = response.json()
  workflows = body["data"]
  workflow_id = workflows[0]["id"] if workflows else None
  next_cursor = body["pagination"]["nextCursor"]
  ```
</CodeGroup>

## Get a workflow

Fetch a simplified workflow graph with node types, connections, and selected
display fields.

<Tip>
  Save `rootNodeId` and the keys in `nodes`. Use them to request full node
  details with [Get workflow node](/docs/api-reference/get-workflow-node).
</Tip>

[API reference](/docs/api-reference/get-workflow)\
[CLI reference](/docs/cli/workflows#get)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops workflows get $workflowId -o json
  ```

  ```js JavaScript theme={"dark"}
  const response = await fetch(
    `https://app.loops.so/api/v1/workflows/${workflowId}`,
    {
      method: "GET",
      headers: {
        "Authorization": "Bearer <your-api-key>",
      },
    },
  );

  const workflow = await response.json();
  const rootNodeId = workflow.rootNodeId;
  const nodes = workflow.nodes;
  const firstNode = nodes[rootNodeId];
  const nextNodeIds = firstNode?.nextNodeIds ?? [];
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const workflow = await loops.getWorkflow(workflowId);
  const rootNodeId = workflow.rootNodeId;
  const nodes = workflow.nodes;
  const firstNode = nodes[rootNodeId];
  const nextNodeIds = firstNode?.nextNodeIds ?? [];
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $workflow = $loops->workflows->get(workflow_id: $workflow_id);
  $root_node_id = $workflow['rootNodeId'];
  $nodes = $workflow['nodes'];
  $first_node = $nodes[$root_node_id];
  $next_node_ids = $first_node['nextNodeIds'] ?? [];
  ```

  ```ruby Ruby SDK theme={"dark"}
  workflow = LoopsSdk::Workflows.get(workflow_id: workflow_id)

  root_node_id = workflow["rootNodeId"]
  nodes = workflow["nodes"]
  first_node = nodes[root_node_id]
  next_node_ids = first_node&.dig("nextNodeIds") || []
  ```

  ```python Python theme={"dark"}
  response = requests.get(
      f"https://app.loops.so/api/v1/workflows/{workflow_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
  )

  workflow = response.json()
  root_node_id = workflow["rootNodeId"]
  nodes = workflow["nodes"]
  first_node = nodes.get(root_node_id)
  next_node_ids = first_node.get("nextNodeIds", []) if first_node else []
  ```
</CodeGroup>

## Get workflow node details

Use the simplified workflow graph to pick a `nodeId`, then fetch the full node
configuration.

[API reference](/docs/api-reference/get-workflow-node)\
[CLI reference](/docs/cli/workflows#nodes-get)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops workflows nodes get $workflowId $nodeId -o json
  ```

  ```js JavaScript theme={"dark"}
  const nodeId = rootNodeId;

  const response = await fetch(
    `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}`,
    {
      method: "GET",
      headers: {
        "Authorization": "Bearer <your-api-key>",
      },
    },
  );

  const node = await response.json();

  if (node.typeName === "EventTrigger") {
    console.log(node.eventName, node.reEligible);
  }

  if (node.typeName === "SendEmailAction") {
    // Use emailMessageId to read or edit the email's content (see below).
    console.log(node.emailMessageId, node.subject);
  }
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const nodeId = rootNodeId;

  const node = await loops.getWorkflowNode(workflowId, nodeId);

  if (node.typeName === "EventTrigger") {
    console.log(node.eventName, node.reEligible);
  }

  if (node.typeName === "SendEmailAction") {
    console.log(node.subject);
  }
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $node_id = $root_node_id;

  $node = $loops->workflows->getNode(
    workflow_id: $workflow_id,
    node_id: $node_id,
  );

  if ($node['typeName'] === 'EventTrigger') {
    echo $node['eventName'], $node['reEligible'];
  }

  if ($node['typeName'] === 'SendEmailAction') {
    echo $node['emailMessageId'], $node['subject'];
  }
  ```

  ```ruby Ruby SDK theme={"dark"}
  node_id = root_node_id

  node = LoopsSdk::Workflows.get_node(
    workflow_id: workflow_id,
    node_id: node_id,
  )

  if node["typeName"] == "EventTrigger"
    puts node["eventName"], node["reEligible"]
  end

  if node["typeName"] == "SendEmailAction"
    puts node["subject"]
  end
  ```

  ```python Python theme={"dark"}
  node_id = root_node_id

  response = requests.get(
      f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
  )

  node = response.json()

  if node["typeName"] == "EventTrigger":
      print(node.get("eventName"), node.get("reEligible"))

  if node["typeName"] == "SendEmailAction":
      # Use emailMessageId to read or edit the email's content (see below).
      print(node.get("emailMessageId"), node.get("subject"))
  ```
</CodeGroup>

## Walk connected nodes

After fetching a node, follow `nextNodeIds` to load the next steps in the
workflow.

<CodeGroup>
  ```bash CLI theme={"dark"}
  for nodeId in $nextNodeIds; do
    loops workflows nodes get $workflowId $nodeId -o json
  done
  ```

  ```js JavaScript theme={"dark"}
  async function getWorkflowNodes(workflowId, nodeIds) {
    const nodes = await Promise.all(
      nodeIds.map(async (nodeId) => {
        const response = await fetch(
          `https://app.loops.so/api/v1/workflows/${workflowId}/nodes/${nodeId}`,
          {
            method: "GET",
            headers: {
              "Authorization": "Bearer <your-api-key>",
            },
          },
        );

        return response.json();
      }),
    );

    return nodes;
  }

  const connectedNodes = await getWorkflowNodes(workflowId, nextNodeIds);
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  async function getWorkflowNodes(workflowId, nodeIds) {
    return Promise.all(
      nodeIds.map((nodeId) => loops.getWorkflowNode(workflowId, nodeId)),
    );
  }

  const connectedNodes = await getWorkflowNodes(workflowId, nextNodeIds);
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  function get_workflow_nodes(LoopsClient $loops, string $workflow_id, array $node_ids): array {
    return array_map(
      fn (string $node_id) => $loops->workflows->getNode(
        workflow_id: $workflow_id,
        node_id: $node_id,
      ),
      $node_ids,
    );
  }

  $connected_nodes = get_workflow_nodes($loops, $workflow_id, $next_node_ids);
  ```

  ```ruby Ruby SDK theme={"dark"}
  def get_workflow_nodes(workflow_id, node_ids)
    node_ids.map do |node_id|
      LoopsSdk::Workflows.get_node(
        workflow_id: workflow_id,
        node_id: node_id,
      )
    end
  end

  connected_nodes = get_workflow_nodes(workflow_id, next_node_ids)
  ```

  ```python Python theme={"dark"}
  def get_workflow_nodes(workflow_id, node_ids):
      nodes = []

      for node_id in node_ids:
          response = requests.get(
              f"https://app.loops.so/api/v1/workflows/{workflow_id}/nodes/{node_id}",
              headers={
                  "Authorization": "Bearer <your-api-key>",
              },
          )
          nodes.append(response.json())

      return nodes

  connected_nodes = get_workflow_nodes(workflow_id, next_node_ids)
  ```
</CodeGroup>

## Edit a workflow email's content

Each `SendEmailAction` node includes an `emailMessageId`. Pass that ID to the
email message content APIs to read or edit the email's subject, LMX body,
sender, and more.

First fetch the current content to get its `contentRevisionId`, then send an
update.

<Tip>
  Save the returned `contentRevisionId` after each update and pass it as
  `expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
  by stale revisions.
</Tip>

Get email message [API reference](/docs/api-reference/get-email-message) [CLI reference](/docs/cli/email-messages#get)\
Update email message [API reference](/docs/api-reference/update-email-message) [CLI reference](/docs/cli/email-messages#update)

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops email-messages get $emailMessageId -o json

  loops email-messages update $emailMessageId \
    --expected-revision-id $contentRevisionId \
    --subject "Welcome to Loops 👋" \
    --lmx '<Paragraph><Text>Thanks for signing up!</Text></Paragraph>'
  ```

  ```js JavaScript theme={"dark"}
  // emailMessageId comes from a SendEmailAction node (see above).
  const emailMessageId = node.emailMessageId;

  // Fetch the current content to get the latest contentRevisionId.
  const current = await fetch(
    `https://app.loops.so/api/v1/email-messages/${emailMessageId}`,
    {
      method: "GET",
      headers: {
        "Authorization": "Bearer <your-api-key>",
      },
    },
  ).then((response) => response.json());

  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: current.contentRevisionId,
        subject: "Welcome to Loops 👋",
        lmx: "<Paragraph>Thanks for signing up!</Paragraph>",
      }),
    },
  );

  const updated = await response.json();
  const nextContentRevisionId = updated.contentRevisionId;
  ```

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

  const loops = new LoopsClient("<your-api-key>");

  const emailMessageId = node.emailMessageId;

  const current = await loops.getEmailMessage(emailMessageId);

  const updated = await loops.updateEmailMessage(emailMessageId, {
    expectedRevisionId: current.contentRevisionId,
    subject: "Welcome to Loops 👋",
    lmx: "<Paragraph><Text>Thanks for signing up!</Text></Paragraph>",
  });

  const nextContentRevisionId = updated.contentRevisionId;
  ```

  ```php PHP SDK theme={"dark"}
  use Loops\LoopsClient;

  $loops = new LoopsClient('<your-api-key>');

  $email_message_id = $node['emailMessageId'];

  $current = $loops->emailMessages->get(email_message_id: $email_message_id);

  $updated = $loops->emailMessages->update(
    email_message_id: $email_message_id,
    expected_revision_id: $current['contentRevisionId'],
    subject: 'Welcome to Loops 👋',
    lmx: '<Paragraph><Text>Thanks for signing up!</Text></Paragraph>',
  );

  $next_content_revision_id = $updated['contentRevisionId'];
  ```

  ```ruby Ruby SDK theme={"dark"}
  email_message_id = node["emailMessageId"]

  current = LoopsSdk::EmailMessages.get(email_message_id: email_message_id)

  updated = LoopsSdk::EmailMessages.update(
    email_message_id: email_message_id,
    expected_revision_id: current["contentRevisionId"],
    subject: "Welcome to Loops 👋",
    lmx: "<Paragraph><Text>Thanks for signing up!</Text></Paragraph>",
  )

  next_content_revision_id = updated["contentRevisionId"]
  ```

  ```python Python theme={"dark"}
  # email_message_id comes from a SendEmailAction node (see above).
  email_message_id = node["emailMessageId"]

  # Fetch the current content to get the latest contentRevisionId.
  current = requests.get(
      f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
      },
  ).json()

  response = requests.post(
      f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
      headers={
          "Authorization": "Bearer <your-api-key>",
          "Content-Type": "application/json",
      },
      json={
          "expectedRevisionId": current["contentRevisionId"],
          "subject": "Welcome to Loops 👋",
          "lmx": "<Paragraph>Thanks for signing up!</Paragraph>",
      },
  )

  updated = response.json()
  next_content_revision_id = updated["contentRevisionId"]
  ```
</CodeGroup>


# Find a contact
Source: https://loops.so/docs/api-reference/find-contact

https://app.loops.so/openapi.json get /v1/contacts/find
Find a contact by email address or user ID.



# Get an audience segment
Source: https://loops.so/docs/api-reference/get-audience-segment

https://app.loops.so/openapi.json get /v1/audience-segments/{audienceSegmentId}
Retrieve a single audience segment by ID.



# Get a campaign
Source: https://loops.so/docs/api-reference/get-campaign

https://app.loops.so/openapi.json get /v1/campaigns/{campaignId}
Retrieve a single campaign by ID.



# Get a campaign group
Source: https://loops.so/docs/api-reference/get-campaign-group

https://app.loops.so/openapi.json get /v1/campaign-groups/{campaignGroupId}
Retrieve a single campaign group by ID.



# Get a component
Source: https://loops.so/docs/api-reference/get-component

https://app.loops.so/openapi.json get /v1/components/{componentId}
Retrieve a single component by ID.



# Get an email message
Source: https://loops.so/docs/api-reference/get-email-message

https://app.loops.so/openapi.json get /v1/email-messages/{emailMessageId}
Retrieve an email message, including its compiled LMX content.



# Get an event pattern by ID
Source: https://loops.so/docs/api-reference/get-event-pattern

https://app.loops.so/openapi.json get /v1/event-patterns/{eventPatternId}
Retrieve event pattern details for a workflow event trigger.



# Get an event pattern by name
Source: https://loops.so/docs/api-reference/get-event-pattern-by-name

https://app.loops.so/openapi.json get /v1/event-patterns/by-name/{eventName}
Retrieve event pattern details by event name for a workflow event trigger. Event names are case-sensitive, so `PaymentReceived` and `paymentReceived` are different events.



# Get a theme
Source: https://loops.so/docs/api-reference/get-theme

https://app.loops.so/openapi.json get /v1/themes/{themeId}
Retrieve a single theme by ID.



# Get a transactional email
Source: https://loops.so/docs/api-reference/get-transactional-email

https://app.loops.so/openapi.json get /v1/transactional-emails/{transactionalId}
Retrieve a single transactional email by ID.



# Get a transactional group
Source: https://loops.so/docs/api-reference/get-transactional-group

https://app.loops.so/openapi.json get /v1/transactional-groups/{transactionalGroupId}
Retrieve a single transactional group by ID.



# Get a workflow
Source: https://loops.so/docs/api-reference/get-workflow

https://app.loops.so/openapi.json get /v1/workflows/{workflowId}
Retrieve a workflow graph with node type names, connections, and selected display fields.



# Get a workflow node
Source: https://loops.so/docs/api-reference/get-workflow-node

https://app.loops.so/openapi.json get /v1/workflows/{workflowId}/nodes/{nodeId}
Retrieve detailed data for a single workflow node.



# API Introduction
Source: https://loops.so/docs/api-reference/intro

Use the Loops REST API to manage contacts, send events, and send transactional email. Learn authentication, rate limits, and request/response examples.

You can use the Loops API to add contacts to your Loops audience,
update their attributes, and send events to Loops.

Prefer a no-code or low-code setup? Use our [integrations](/docs/integrations) (like [Zapier](/docs/integrations/zapier) or [Make](/docs/integrations/make)), or trigger workflows using [incoming webhooks](/docs/integrations/incoming-webhooks).

## Authentication

<Warning>
  Your Loops API key should never be used client side or exposed to your end
  users.
</Warning>

<Accordion title="Authentication Steps">
  To get started, you'll need an API key. Go to [Settings -> API](https://app.loops.so/settings?page=api) in Loops and click **Generate key**.

  <img alt="Generate API key button in Loops settings" />

  This creates an API key. You can assign it a human-readable name:

  <img alt="API key name field when creating a key" />

  We suggest using a different API key for different purposes. You can revoke an API key at any time with the trash icon.

  When making an API call, add an Authorization header and set the API key as a Bearer token:

  <img alt="Example API request showing Authorization Bearer header" />

  You can test your API key by making a `GET` request to

  ```
  https://app.loops.so/api/v1/api-key
  ```

  If successful, you will receive the following response:

  ```
  {
    "success": true,
    "teamName": "Company name"
  }
  ```

  Here's an example Curl request (replace `d2d561f5ff80136f69b4b5a31b9fb3c9` with your own API key):

  ```bash theme={"dark"}
  curl https://app.loops.so/api/v1/api-key -H "Accept: application/json" -H "Authorization: Bearer d2d561f5ff80136f69b4b5a31b9fb3c9"
  ```
</Accordion>

## API Reference

The base URL for the API is `https://app.loops.so/api`

### API key

Test your API key.

<CardGroup>
  <Card title="Test API key" icon="square-terminal" href="/api-reference/api-key" />
</CardGroup>

### Audience segments

View audience segments for campaign targeting.

<CardGroup>
  <Card title="Get audience segment" icon="users" href="/api-reference/get-audience-segment" />

  <Card title="List audience segments" icon="users" href="/api-reference/list-audience-segments" />

  <Card title="Create audience segment" icon="users" href="/api-reference/create-audience-segment" />
</CardGroup>

### Campaign groups

Organize campaigns into groups.

<CardGroup>
  <Card title="Create campaign group" icon="folder" href="/api-reference/create-campaign-group" />

  <Card title="Update campaign group" icon="folder" href="/api-reference/update-campaign-group" />

  <Card title="Get campaign group" icon="folder" href="/api-reference/get-campaign-group" />

  <Card title="List campaign groups" icon="folder" href="/api-reference/list-campaign-groups" />
</CardGroup>

### Campaigns

Create and manage campaigns.

<CardGroup>
  <Card title="Create campaign" icon="envelope-open" href="/api-reference/create-campaign" />

  <Card title="Update campaign" icon="envelope-open" href="/api-reference/update-campaign" />

  <Card title="Get campaign" icon="envelope-open" href="/api-reference/get-campaign" />

  <Card title="List campaigns" icon="envelope-open" href="/api-reference/list-campaigns" />
</CardGroup>

### Configuration

View configuration settings.

<CardGroup>
  <Card title="List dedicated sending IP addresses" icon="network-wired" href="/api-reference/dedicated-sending-ips" />
</CardGroup>

### Contact properties

Manage contact properties.

<CardGroup>
  <Card title="Create contact property" icon="address-book" href="/api-reference/create-contact-property" />

  <Card title="List contact properties" icon="address-book" href="/api-reference/list-contact-properties" />
</CardGroup>

### Contacts

Manage contacts.

<CardGroup>
  <Card title="Create contact" icon="user-plus" href="/api-reference/create-contact" />

  <Card title="Update contact" icon="user-pen" href="/api-reference/update-contact" />

  <Card title="Find contact" icon="user-magnifying-glass" href="/api-reference/find-contact" />

  <Card title="Delete contact" icon="user-xmark" href="/api-reference/delete-contact" />

  <Card title="Check contact suppression status" icon="circle-xmark" href="/api-reference/check-contact-suppression" />

  <Card title="Remove suppression for a contact" icon="circle-check" href="/api-reference/remove-contact-suppression" />
</CardGroup>

### Components

Reusable components for your emails.

<CardGroup>
  <Card title="Create component" icon="shapes" href="/api-reference/create-component" />

  <Card title="Update component" icon="shapes" href="/api-reference/update-component" />

  <Card title="Get component" icon="shapes" href="/api-reference/get-component" />

  <Card title="List components" icon="shapes" href="/api-reference/list-components" />
</CardGroup>

### Email messages

Manage the content of campaigns and transactional emails.

<CardGroup>
  <Card title="Update email message" icon="file-lines" href="/api-reference/update-email-message" />

  <Card title="Get email message" icon="file-lines" href="/api-reference/get-email-message" />

  <Card title="Send a preview email message" icon="file-lines" href="/api-reference/preview-email-message" />

  <Card title="Run Guardian checks" icon="file-lines" href="/api-reference/run-guardian-checks" />
</CardGroup>

### Event patterns

View workflow event patterns.

<CardGroup>
  <Card title="List event patterns" icon="rectangle-list" href="/api-reference/list-event-patterns" />

  <Card title="Get event pattern" icon="rectangle-list" href="/api-reference/get-event-pattern" />

  <Card title="Get event pattern by name" icon="rectangle-list" href="/api-reference/get-event-pattern-by-name" />
</CardGroup>

### Events

Send events to trigger workflows.

<CardGroup>
  <Card title="Send event" icon="bolt" href="/api-reference/send-event" />
</CardGroup>

### Mailing lists

View your mailing lists.

<CardGroup>
  <Card title="List mailing lists" icon="square-list" href="/api-reference/list-mailing-lists" />
</CardGroup>

### Themes

Retrieve email themes.

<CardGroup>
  <Card title="Create theme" icon="palette" href="/api-reference/create-theme" />

  <Card title="Update theme" icon="palette" href="/api-reference/update-theme" />

  <Card title="Get theme" icon="palette" href="/api-reference/get-theme" />

  <Card title="List themes" icon="palette" href="/api-reference/list-themes" />
</CardGroup>

### Transactional emails

Create, manage and send transactional emails.

<CardGroup>
  <Card title="Send transactional email" icon="envelope" href="/api-reference/send-transactional-email" />

  <Card title="Create transactional email" icon="envelope" href="/api-reference/create-transactional-email" />

  <Card title="Update transactional email" icon="envelope" href="/api-reference/update-transactional-email" />

  <Card title="Ensure transactional email has draft" icon="envelope" href="/api-reference/ensure-transactional-draft" />

  <Card title="Publish transactional email" icon="envelope" href="/api-reference/publish-transactional-email" />

  <Card title="Get transactional email" icon="envelope" href="/api-reference/get-transactional-email" />

  <Card title="List transactional emails" icon="envelope" href="/api-reference/list-transactional-emails" />
</CardGroup>

### Transactional groups

Organize transactional emails into groups.

<CardGroup>
  <Card title="Create transactional group" icon="folder" href="/api-reference/create-transactional-group" />

  <Card title="Update transactional group" icon="folder" href="/api-reference/update-transactional-group" />

  <Card title="Get transactional group" icon="folder" href="/api-reference/get-transactional-group" />

  <Card title="List transactional groups" icon="folder" href="/api-reference/list-transactional-groups" />
</CardGroup>

### Uploads

Upload images for email messages.

<CardGroup>
  <Card title="Create upload" icon="file-arrow-up" href="/api-reference/create-upload" />

  <Card title="Complete upload" icon="file-arrow-up" href="/api-reference/complete-upload" />
</CardGroup>

### Workflow nodes

View and mutate workflow nodes.

<CardGroup>
  <Card title="Create workflow node" icon="diagram-project" href="/api-reference/create-workflow-node" />

  <Card title="Add a branch" icon="diagram-project" href="/api-reference/add-workflow-branch" />

  <Card title="Update workflow node" icon="diagram-project" href="/api-reference/update-workflow-node" />

  <Card title="Get workflow node" icon="diagram-project" href="/api-reference/get-workflow-node" />

  <Card title="Delete workflow node" icon="diagram-project" href="/api-reference/delete-workflow-node" />

  <Card title="Delete workflow nodes recursively" icon="diagram-project" href="/api-reference/delete-workflow-nodes" />
</CardGroup>

### Workflows

View and manage workflows.

<CardGroup>
  <Card title="Create workflow" icon="arrows-rotate" href="/api-reference/create-workflow" />

  <Card title="Update workflow" icon="arrows-rotate" href="/api-reference/update-workflow" />

  <Card title="Get workflow" icon="arrows-rotate" href="/api-reference/get-workflow" />

  <Card title="List workflows" icon="arrows-rotate" href="/api-reference/list-workflows" />

  <Card title="Change workflow mailing list" icon="arrows-rotate" href="/api-reference/change-workflow-mailing-list" />
</CardGroup>

## Rate Limiting

To ensure the quality of service for all users, our API is rate limited. This means there's a limit to the number of requests your application can make to our API in a certain time frame.

The baseline rate limit is **10 requests per second per team**.

For the content API, the baseline rate limit is **60 requests per 60 seconds per team**. This applies to `/v1/campaigns/*`, `/v1/campaign-groups/*`, `/v1/components/*`, `/v1/email-messages/*`, `/v1/themes/*`, `/v1/transactional-emails/*`, `/v1/transactional-groups/*`, `/v1/uploads/*`, and `/v1/workflows/*` endpoints.

<Accordion title="Rate Limiting Details">
  To see your current usage, we provide the following response headers in every API response:

  * `x-ratelimit-limit`: The maximum number of requests you can make per second.
  * `x-ratelimit-remaining`: The number of requests remaining in the current rate limit window.

  Here is an example of a successful response with rate limit headers:

  <img alt="Example API response showing rate limit headers" />

  If you exceed this limit, you'll receive a response with HTTP status `429 Too Many Requests`. Here is an example of a failed response with rate limit headers:

  <img alt="Example 429 rate limit response with headers" />

  It's important to handle these 429 responses in your application. We recommend implementing retries with an exponential backoff strategy.

  If your use case requires a higher limit, please [contact support](https://app.loops.so/settings?page=support) and we'll be happy to discuss your needs.
</Accordion>

## Debugging

Sometimes things go wrong. Here are some tips to help you debug your API requests.

<Accordion title="Debugging Steps">
  If you are having trouble with the API, we recommend using a tool like [Postman](https://www.postman.com/) to test your requests.

  ### Handling CORS errors

  The Loops API does not support cross-origin requests made from client-side JavaScript. To avoid CORS errors, make sure to issue your requests from a server-side application.

  ### Dealing with `401 Unauthorized` "Invalid API key" errors

  Make sure you have generated an API key from [Settings -> API](https://app.loops.so/settings?page=api) and that you are including it in your requests.

  Your API key should be included in the "Authorization" header of your request, following the format `Authorization: Bearer YOUR_API_KEY`.

  ### Handling rate limiting (`429` Responses)

  The Loops API allows a maximum of 10 requests per second per team. If you receive a `429 Too Many Requests` response, this means you have exceeded this limit.

  The `x-ratelimit-limit` and `x-ratelimit-remaining` headers in the response can provide information about your current rate limit usage.

  ### Handling other `400`-level Responses

  `400`-level responses typically indicate that there was a problem with the request. The response body will contain more information about what went wrong, so be sure to check it for details.

  Check on your request type (GET, POST, PUT, DELETE) and ensure that you are using the correct one for the endpoint you are trying to access.

  ### "Some body key or value is longer than allowable"

  If you receive this error, it means that a value in the request body is too long. We support a maximum of 500 characters for each value, including the opening and closing quotes. Please reduce the length of the values in your request and try again.

  ***

  If you have followed these steps and are still experiencing issues, don't hesitate to [reach out to the Loops team](https://app.loops.so/settings?page=support) for further assistance.
</Accordion>

## OpenAPI spec

Get started quickly with the Loops API using our OpenAPI documents.

You can import these documents into an API client like Postman or Insomnia to see and use all of our endpoints, with example requests and expected responses.

* **YAML:** [app.loops.so/openapi.yaml](https://app.loops.so/openapi.yaml)
* **JSON:** [app.loops.so/openapi.json](https://app.loops.so/openapi.json)

## SDKs

SDKs are software packages built on top of the API, making it easier to integrate into your project.

<CardGroup>
  <Card title="Go" icon="golang" href="/sdks/go">
    The official Go SDK for Loops.
  </Card>

  <Card title="JavaScript" icon="js" href="/sdks/javascript">
    The official JavaScript/TypeScript SDK for Loops.
  </Card>

  <Card title="Nuxt" icon={<svg viewBox="0 0 61 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M33.9971 39.539H56.5247C57.2402 39.539 57.9432 39.3564 58.5628 39.0093C59.1825 38.6623 59.697 38.1631 60.0546 37.5619C60.4122 36.9608 60.6003 36.2789 60.6 35.5849C60.5997 34.8908 60.411 34.2091 60.0528 33.6083L44.9239 8.1904C44.5663 7.5894 44.0519 7.09032 43.4324 6.74332C42.8129 6.39632 42.1101 6.21363 41.3947 6.21363C40.6793 6.21363 39.9766 6.39632 39.357 6.74332C38.7375 7.09032 38.2231 7.5894 37.8655 8.1904L33.9971 14.6939L26.4338 1.97648C26.0759 1.37553 25.5613 0.876518 24.9416 0.52958C24.3219 0.182643 23.619 0 22.9035 0C22.188 0 21.4851 0.182643 20.8654 0.52958C20.2457 0.876518 19.7311 1.37553 19.3732 1.97648L0.547184 33.6083C0.189033 34.2091 0.00031171 34.8908 3.85805e-07 35.5849C-0.000310938 36.2789 0.187799 36.9608 0.54541 37.5619C0.903021 38.1631 1.41752 38.6623 2.03717 39.0093C2.65681 39.3564 3.35975 39.539 4.07528 39.539H18.2162C23.819 39.539 27.9509 37.1518 30.794 32.4945L37.6965 20.8993L41.3936 14.6939L52.4895 33.3335H37.6965L33.9971 39.539ZM17.9857 33.3272L8.11711 33.325L22.9101 8.47362L30.2912 20.8993L25.3492 29.2044C23.4611 32.2262 21.3162 33.3272 17.9857 33.3272Z" fill="#FF4A00"/></svg>} href="/sdks/nuxt">
    The official Nuxt module for Loops.
  </Card>

  <Card title="PHP" icon="php" href="/sdks/php">
    The official PHP SDK for Loops.
  </Card>

  <Card title="Ruby" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="#FF4A00"><path d="M 3.125 23.160156 L 8.183594 14.480469 C 8.230469 14.394531 8.292969 14.316406 8.363281 14.253906 L 17.441406 5.792969 C 17.539063 5.703125 17.65625 5.632813 17.78125 5.585938 L 25.578125 2.722656 L 32.152344 7.316406 L 28.878906 17.722656 L 16.597656 29.132813 L 6.582031 31.378906 Z M 7.296875 33.269531 L 11.96875 46.628906 L 15.8125 31.355469 Z M 42.484375 2.875 L 35.074219 6.992188 L 47.355469 15.910156 C 47.613281 14.761719 47.914063 13.195313 47.984375 11.863281 C 47.980469 11.847656 47.980469 11.828125 47.980469 11.808594 C 48.019531 9.910156 47.707031 7.925781 46.675781 6.210938 C 45.75 4.667969 44.359375 3.539063 42.535156 2.828125 C 42.519531 2.84375 42.507813 2.863281 42.484375 2.875 Z M 34.375 35.578125 L 29.296875 20.058594 L 18.988281 29.644531 Z M 31.144531 19.269531 L 36.1875 34.679688 C 40.132813 30.386719 44.171875 25.246094 46.476563 18.925781 Z M 45.382813 16.949219 L 33.851563 8.578125 L 31.117188 17.269531 Z M 5.761719 34.9375 L 2.335938 40.816406 C 3.507813 46.242188 7.566406 47.457031 10.242188 47.753906 Z M 4.878906 32.484375 L 2 25.644531 L 2 37.421875 Z M 39.921875 2.011719 C 39.90625 2.007813 39.890625 2 39.871094 2 L 28.039063 2 L 33.3125 5.6875 Z M 17.875 31.355469 L 13.777344 47.632813 C 22.109375 46.40625 28.359375 41.929688 33.425781 37.355469 Z M 44.546875 45.648438 C 44.3125 45.648438 44.074219 45.566406 43.882813 45.398438 L 35.617188 38.058594 C 31.984375 41.429688 27.742188 44.855469 22.519531 47.164063 L 44.5625 45.667969 C 44.691406 45.660156 44.8125 45.621094 44.925781 45.570313 C 44.804688 45.621094 44.675781 45.648438 44.546875 45.648438 Z M 37.0625 36.667969 L 45.210938 43.902344 C 45.382813 44.054688 45.472656 44.253906 45.511719 44.460938 L 47.195313 22.414063 C 44.582031 28.078125 40.742188 32.730469 37.0625 36.667969 Z"></path></svg>} href="/sdks/ruby">
    The official Ruby SDK for Loops.
  </Card>
</CardGroup>

### Unofficial SDKs

The following SDKs are community-submitted and have not been officially reviewed or endorsed by Loops. We recommend thoroughly testing and reviewing the code before integrating it into your project.

* [Laravel](https://github.com/plutolinks/laravel-loops) by PlutoLinks
* [PHP](https://github.com/plutolinks/loops-php) by PlutoLinks
* [Ruby on Rails](https://github.com/danielfriis/loops_rails) by Daniel Friis

[Submit an SDK](mailto:dan@loops.so)


# List audience segments
Source: https://loops.so/docs/api-reference/list-audience-segments

https://app.loops.so/openapi.json get /v1/audience-segments
Retrieve a paginated list of audience segments, most recently created first.



# List campaign groups
Source: https://loops.so/docs/api-reference/list-campaign-groups

https://app.loops.so/openapi.json get /v1/campaign-groups
Retrieve a paginated list of campaign groups, most recently created first.



# List campaigns
Source: https://loops.so/docs/api-reference/list-campaigns

https://app.loops.so/openapi.json get /v1/campaigns
Retrieve a paginated list of campaigns.



# List components
Source: https://loops.so/docs/api-reference/list-components

https://app.loops.so/openapi.json get /v1/components
Retrieve a paginated list of email components.



# List contact properties
Source: https://loops.so/docs/api-reference/list-contact-properties

https://app.loops.so/openapi.json get /v1/contacts/properties
Retrieve a list of your account's contact properties.



# List event patterns
Source: https://loops.so/docs/api-reference/list-event-patterns

https://app.loops.so/openapi.json get /v1/event-patterns
Retrieve a paginated list of event patterns available to workflow event trigger nodes.



# List mailing lists
Source: https://loops.so/docs/api-reference/list-mailing-lists

https://app.loops.so/openapi.json get /v1/lists
Retrieve a list of your account's mailing lists.



# List themes
Source: https://loops.so/docs/api-reference/list-themes

https://app.loops.so/openapi.json get /v1/themes
Retrieve a paginated list of email themes, most recently created first.



# List transactional emails
Source: https://loops.so/docs/api-reference/list-transactional-emails

https://app.loops.so/openapi.json get /v1/transactional-emails
Retrieve a paginated list of transactional emails, most recently created first.



# List transactional emails
Source: https://loops.so/docs/api-reference/list-transactional-emails-v1

https://app.loops.so/openapi.json get /v1/transactional
Get a list of published transactional emails.



# List transactional groups
Source: https://loops.so/docs/api-reference/list-transactional-groups

https://app.loops.so/openapi.json get /v1/transactional-groups
Retrieve a paginated list of transactional groups, most recently created first.



# List workflows
Source: https://loops.so/docs/api-reference/list-workflows

https://app.loops.so/openapi.json get /v1/workflows
Retrieve a paginated list of workflows.



# Send a preview of an email message
Source: https://loops.so/docs/api-reference/preview-email-message

https://app.loops.so/openapi.json post /v1/email-messages/{emailMessageId}/preview
Send a test preview of an email message to one or more addresses.



# Publish a transactional email
Source: https://loops.so/docs/api-reference/publish-transactional-email

https://app.loops.so/openapi.json post /v1/transactional-emails/{transactionalId}/publish
Publish the transactional email's current draft email message. The draft becomes the published version and the draft is cleared.



# Remove contact from suppression
Source: https://loops.so/docs/api-reference/remove-contact-suppression

https://app.loops.so/openapi.json delete /v1/contacts/suppression
Remove suppression for a contact by email address or user ID.



# Reroute a node connection
Source: https://loops.so/docs/api-reference/reroute-node-connection

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}/nodes/{nodeId}/reroute
Reroute one existing node connection to another valid target node.



# Run Guardian checks on an email message
Source: https://loops.so/docs/api-reference/run-guardian-checks

https://app.loops.so/openapi.json get /v1/email-messages/{emailMessageId}/guardian
Run Guardian content validation on an email message and return errors and warnings before you publish or send.



# Send an event
Source: https://loops.so/docs/api-reference/send-event

https://app.loops.so/openapi.json post /v1/events/send
Send events to trigger workflows.



# Send a transactional email
Source: https://loops.so/docs/api-reference/send-transactional-email

https://app.loops.so/openapi.json post /v1/transactional
Send a transactional email.



# Update a campaign
Source: https://loops.so/docs/api-reference/update-campaign

https://app.loops.so/openapi.json post /v1/campaigns/{campaignId}
Update a draft campaign's name, group, audience (mailing list, segment, or filter), or scheduling.



# Update a campaign group
Source: https://loops.so/docs/api-reference/update-campaign-group

https://app.loops.so/openapi.json post /v1/campaign-groups/{campaignGroupId}
Update a campaign group's name or description.



# Update a component
Source: https://loops.so/docs/api-reference/update-component

https://app.loops.so/openapi.json post /v1/components/{componentId}
Update a component's name and/or LMX body. Body changes cascade to every email using the component.



# Update a contact
Source: https://loops.so/docs/api-reference/update-contact

https://app.loops.so/openapi.json put /v1/contacts/update
Update or create a contact.



# Update an email message
Source: https://loops.so/docs/api-reference/update-email-message

https://app.loops.so/openapi.json post /v1/email-messages/{emailMessageId}
Update an email message's subject, preview text, sender, or LMX content.



# Update a theme
Source: https://loops.so/docs/api-reference/update-theme

https://app.loops.so/openapi.json post /v1/themes/{themeId}
Update a theme's name and/or styles. Style changes cascade to every email using the theme.



# Update a transactional email
Source: https://loops.so/docs/api-reference/update-transactional-email

https://app.loops.so/openapi.json post /v1/transactional-emails/{transactionalId}
Update a transactional email by ID.



# Update a transactional group
Source: https://loops.so/docs/api-reference/update-transactional-group

https://app.loops.so/openapi.json post /v1/transactional-groups/{transactionalGroupId}
Update a transactional group's name or description. At least one field must be provided. The reserved "Unsorted" group cannot be edited.



# Update a workflow
Source: https://loops.so/docs/api-reference/update-workflow

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}
Update a workflow's display properties.



# Update a workflow node
Source: https://loops.so/docs/api-reference/update-workflow-node

https://app.loops.so/openapi.json post /v1/workflows/{workflowId}/nodes/{nodeId}
Update workflow-node-owned fields for a single node.



# Loops CLI
Source: https://loops.so/docs/cli

The official Loops command-line interface. Manage contacts, send events, workflows, and deliver transactional emails directly from your terminal.

## Installation

### One-line installer

```bash theme={"dark"}
curl -fsSL https://install.loops.so/cli | sh
```

To install a specific version or to a custom path, add `-s -- <version> <path>` after `sh` in the command above. The default installation path is `~/.local/bin`.

<Tip>
  Using a coding agent? Install the Loops CLI and agent skills together with
  `curl -fsSL https://install.loops.so/wizard | sh`.
</Tip>

### Homebrew

```bash theme={"dark"}
brew install loops-so/tap/loops
```

### Script for Windows PowerShell

```powershell theme={"dark"}
irm https://raw.githubusercontent.com/loops-so/cli/main/install.ps1 | iex
```

### Go install

```bash theme={"dark"}
go install github.com/loops-so/cli/cmd/loops@latest
```

### Verify

```bash theme={"dark"}
loops --version
```

## Authentication

The CLI requires a Loops API key. Get one from [Settings > API](https://app.loops.so/settings?page=api) in Loops.

There are two ways to authenticate the CLI.

### Keyring storage

Store a key locally with `loops auth login`:

```bash theme={"dark"}
loops auth login my-team
```

You can store keys for multiple teams and switch between them:

* `loops auth use <name>` — set a stored key as the default
* `loops auth list` — list stored keys and see which is the default
* `loops auth get <name>` — print a stored API key
* `--team <name>` on any command — use a specific stored key for that command

See [Auth commands](/docs/cli/auth) for the full reference.

### Environment variable

Set `LOOPS_API_KEY` to use a key directly — useful for CI or when keyring storage isn't available.

```bash theme={"dark"}
export LOOPS_API_KEY=your_api_key
```

### Precedence

When multiple keys are available, the CLI resolves them in this order:

1. `LOOPS_API_KEY` environment variable
2. `--team` flag
3. The current default (set via `loops auth use`)

## Global flags

These flags can be used with any command.

| Flag       | Short | Description                               |
| ---------- | ----- | ----------------------------------------- |
| `--debug`  |       | Print API request details before sending  |
| `--output` | `-o`  | Output format: `text` (default) or `json` |
| `--team`   | `-t`  | Team key name to use                      |
| `--help`   | `-h`  | Show help for any command                 |

## Commands

<CardGroup>
  <Card title="auth" href="/cli/auth" icon="key">
    Log in, switch teams, and manage stored API keys.
  </Card>

  <Card title="campaigns" href="/cli/campaigns" icon="paper-plane">
    Create, target, schedule, and update draft campaigns.
  </Card>

  <Card title="audience-segments" href="/cli/audience-segments" icon="chart-pie">
    Read and create audience segments.
  </Card>

  <Card title="campaign-groups" href="/cli/campaign-groups" icon="layer-group">
    Organize campaigns into groups.
  </Card>

  <Card title="contact-properties" href="/cli/contact-properties" icon="address-book">
    Create and list contact properties.
  </Card>

  <Card title="contacts" href="/cli/contacts" icon="user">
    Create, update, find, and delete contacts.
  </Card>

  <Card title="completion" href="/cli/completion" icon="square-terminal">
    Set up shell autocompletion.
  </Card>

  <Card title="components" href="/cli/components" icon="puzzle-piece">
    View, create, and update email components.
  </Card>

  <Card title="email-messages" href="/cli/email-messages" icon="message-pen">
    Get, preview, and update draft email messages.
  </Card>

  <Card title="events" href="/cli/events" icon="bolt">
    Send events to trigger automations.
  </Card>

  <Card title="event-patterns" href="/cli/event-patterns" icon="wave-square">
    Read event patterns.
  </Card>

  <Card title="lists" href="/cli/lists" icon="list">
    View your mailing lists.
  </Card>

  <Card title="themes" href="/cli/themes" icon="palette">
    View, create, and update email themes.
  </Card>

  <Card title="transactional" href="/cli/transactional" icon="envelope">
    Send and manage transactional emails.
  </Card>

  <Card title="transactional-groups" href="/cli/transactional-groups" icon="boxes-stacked">
    Organize transactional emails into groups.
  </Card>

  <Card title="uploads" href="/cli/uploads" icon="cloud-arrow-up">
    Upload files as email assets.
  </Card>

  <Card title="workflows" href="/cli/workflows" icon="arrows-rotate">
    List, create, and edit workflows and their nodes.
  </Card>
</CardGroup>

## Utility commands

### `loops api-key`

Validate your current API key.

```bash theme={"dark"}
loops api-key
```

### `loops dedicated-sending-ips`

List the dedicated sending IP addresses assigned to your team.

```bash theme={"dark"}
loops dedicated-sending-ips
```

### `loops skill install`

Install the [Loops CLI skill](/docs/skills) so AI coding agents can discover and use the CLI. Pass `--all` to install every Loops skill, `--global` to install it globally, and `-y` to skip confirmation prompts.

```bash theme={"dark"}
loops skill install
```

### `loops --version`

Print the CLI version.

```bash theme={"dark"}
loops --version
```

### `loops agent-context`

Print a JSON description of every command, subcommand, and flag in the CLI. Designed for AI coding agents (and other tools) that need to discover what the CLI can do without scraping `--help` output.

```bash theme={"dark"}
loops agent-context
```


# Audience segments
Source: https://loops.so/docs/cli/audience-segments

Read and create audience segments.

The `audience-segments` command reads and creates audience segments, which are used to target [campaigns](/docs/cli/campaigns) and for filtering inside [workflows](/docs/cli/workflows).

## `create`

Create a new audience segment. Provide the audience filter inline with `--filter` or from a JSON file with `--filter-file`.

Set the filter inline with `--filter`:

```bash theme={"dark"}
loops audience-segments create \
  --name "Pro plan contacts" \
  --filter '{"match":"all","conditions":[{"type":"property","key":"plan","operator":"equals","value":"pro"}]}'
```

Alternatively, load the filter from a JSON file with `--filter-file`:

```json filter.json theme={"dark"}
{
  "match": "all",
  "conditions": [
    { "type": "property", "key": "plan", "operator": "equals", "value": "pro" }
  ]
}
```

```bash theme={"dark"}
loops audience-segments create \
  --name "Pro plan contacts" \
  --filter-file ./filter.json
```

The full audience filter schema is documented in the [API reference](/docs/api-reference/create-audience-segment).

### Flags

| Flag            | Short | Description                                  |
| --------------- | ----- | -------------------------------------------- |
| `--name`        | `-n`  | Segment name <Badge>Required</Badge>         |
| `--description` |       | Segment description                          |
| `--filter`      |       | Audience filter as an inline JSON string     |
| `--filter-file` |       | Path to a JSON file with the audience filter |

## `get`

Get an audience segment by ID.

```bash theme={"dark"}
loops audience-segments get cln0y4p6r003yl70i1j2k3l4m
```

## `list`

List audience segments.

```bash theme={"dark"}
loops audience-segments list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected audience segment ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops audience-segments list --pick
```

### Flags

| Flag         | Description                                                                                                               |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick an audience segment and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                                      |
| `--cursor`   | Pagination cursor for a specific page                                                                                     |


# Auth
Source: https://loops.so/docs/cli/auth

Log in, switch teams, and manage stored API keys.

The `auth` command manages your CLI authentication. API keys are stored locally so you can switch between teams without re-entering credentials.

## `login`

Authenticate with your Loops API key. Pass a name of your choosing — you'll use it later with `auth use`, `auth logout`, and the `--team` flag to refer to this key. You'll be prompted to paste the key interactively.

```
loops auth login <name> [--flags]
```

### Flags

| Flag            | Description                           |
| --------------- | ------------------------------------- |
| `--skip-verify` | Save the API key without verifying it |

## `list`

List all stored API keys.

```bash theme={"dark"}
loops auth list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and switch to the selected team (equivalent to `loops auth use <name>`). Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops auth list --pick
```

### Flags

| Flag     | Description                                                                              |
| -------- | ---------------------------------------------------------------------------------------- |
| `--pick` | Interactively pick a team to switch to. Requires [fzf](https://github.com/junegunn/fzf). |

## `get`

Print a stored API key by name. In text output, only the key is printed — useful for piping into scripts or other tools.

```bash theme={"dark"}
loops auth get my-team
```

Use `--output json` to get the key name and value together:

```bash theme={"dark"}
loops auth get my-team --output json
```

<Warning>
  This command prints your API key to stdout. Be careful when using it in shared terminals or scripts that log output.
</Warning>

## `status`

Print the resolved configuration, including which API key is currently active.

```bash theme={"dark"}
loops auth status
```

## `use`

Set or clear the active API key. Pass the name of a stored key to switch to it.

```
loops auth use <name> [--flags]
```

To clear the active team selection:

```bash theme={"dark"}
loops auth use --clear
```

### Flags

| Flag      | Description           |
| --------- | --------------------- |
| `--clear` | Clear the active team |

## `logout`

Remove a stored API key.

```
loops auth logout <name>
```


# Campaign groups
Source: https://loops.so/docs/cli/campaign-groups

Create, list, and update campaign groups.

The `campaign-groups` command manages campaign groups, which are used to organize [campaigns](/docs/cli/campaigns).

## `create`

Create a new campaign group.

```bash theme={"dark"}
loops campaign-groups create --name "Product updates"
```

### Flags

| Flag            | Short | Description                                                                          |
| --------------- | ----- | ------------------------------------------------------------------------------------ |
| `--name`        | `-n`  | Campaign group name. Cannot be the reserved name "Unsorted". <Badge>Required</Badge> |
| `--description` | `-d`  | Campaign group description                                                           |

## `get`

Get a campaign group by ID.

```bash theme={"dark"}
loops campaign-groups get cln0y4p6r003yl70i1j2k3l4m
```

## `list`

List campaign groups.

```bash theme={"dark"}
loops campaign-groups list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected campaign group ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops campaign-groups list --pick
```

### Flags

| Flag         | Description                                                                                                            |
| ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a campaign group and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                                   |
| `--cursor`   | Pagination cursor for a specific page                                                                                  |

## `update`

Update a campaign group.

```bash theme={"dark"}
loops campaign-groups update cln0y4p6r003yl70i1j2k3l4m --name "Weekly digest"
```

### Flags

| Flag            | Short | Description                                       |
| --------------- | ----- | ------------------------------------------------- |
| `--name`        | `-n`  | New name. Cannot be the reserved name "Unsorted". |
| `--description` | `-d`  | New description                                   |


# Campaigns
Source: https://loops.so/docs/cli/campaigns

Create, list, and update draft campaigns.

The `campaigns` command lets you manage [campaigns](/docs/api-reference/create-campaign) in your Loops account.

## `create`

Create a new draft campaign. This also creates an empty email message — use [`email-messages update`](/docs/cli/email-messages#update) to set its subject, sender, preview text, and LMX content.

```bash theme={"dark"}
loops campaigns create --name "March product update"
```

Target the campaign at an [audience segment](/docs/cli/audience-segments) and schedule it to send at a specific time:

```bash theme={"dark"}
loops campaigns create \
  --name "March product update" \
  --audience-segment-id cln0y4p6r003yl70i1j2k3l4m \
  --schedule-at 2026-07-01T12:00:00Z
```

### Flags

| Flag                     | Short | Description                                                                      |
| ------------------------ | ----- | -------------------------------------------------------------------------------- |
| `--name`                 | `-n`  | Campaign name <Badge>Required</Badge>                                            |
| `--audience-segment-id`  |       | [Audience segment](/docs/cli/audience-segments) ID to target. Pass `"null"` to clear. |
| `--campaign-group-id`    |       | [Campaign group](/docs/cli/campaign-groups) ID                                        |
| `--mailing-list-id`      |       | Mailing list ID to target. Pass `"null"` to clear.                               |
| `--audience-filter`      |       | Inline JSON with an ad-hoc audience filter. Pass `"null"` to clear.              |
| `--audience-filter-file` |       | Path to a JSON file with an ad-hoc audience filter                               |
| `--schedule-at`          |       | Send at the given ISO 8601 timestamp (e.g. `2026-07-01T12:00:00Z`)               |
| `--schedule-now`         |       | Send immediately when published                                                  |

<Note>
  `--audience-filter` and `--audience-filter-file` are two ways to supply the same ad-hoc filter, so they're mutually exclusive with each other — use `--audience-filter` for short inline JSON and `--audience-filter-file` to load it from a file. A filter can be combined with `--audience-segment-id`: when both are set, the filter is applied on top of the segment — contacts must match both. Setting a segment without a filter (or a filter without a segment) clears the other, so pass both together to keep both.
</Note>

## `get`

Get a campaign by ID.

```bash theme={"dark"}
loops campaigns get cln0y4p6r003yl70i1j2k3l4m
```

## `list`

List campaigns.

```bash theme={"dark"}
loops campaigns list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected campaign ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops campaigns list --pick
```

### Flags

| Flag         | Description                                                                                                      |
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a campaign and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                             |
| `--cursor`   | Pagination cursor for a specific page                                                                            |

## `update`

Update a draft campaign.

```bash theme={"dark"}
loops campaigns update cln0y4p6r003yl70i1j2k3l4m --name "April product update"
```

Retarget an existing campaign at a [campaign group](/docs/cli/campaign-groups) and send it immediately when published:

```bash theme={"dark"}
loops campaigns update cln0y4p6r003yl70i1j2k3l4m \
  --campaign-group-id clr4c8t0v008yl70x3y4z5a6b \
  --schedule-now
```

### Flags

| Flag                     | Short | Description                                                                      |
| ------------------------ | ----- | -------------------------------------------------------------------------------- |
| `--name`                 | `-n`  | Campaign name                                                                    |
| `--audience-segment-id`  |       | [Audience segment](/docs/cli/audience-segments) ID to target. Pass `"null"` to clear. |
| `--campaign-group-id`    |       | [Campaign group](/docs/cli/campaign-groups) ID                                        |
| `--mailing-list-id`      |       | Mailing list ID to target. Pass `"null"` to clear.                               |
| `--audience-filter`      |       | Inline JSON with an ad-hoc audience filter. Pass `"null"` to clear.              |
| `--audience-filter-file` |       | Path to a JSON file with an ad-hoc audience filter                               |
| `--schedule-at`          |       | Send at the given ISO 8601 timestamp (e.g. `2026-07-01T12:00:00Z`)               |
| `--schedule-now`         |       | Send immediately when published                                                  |

<Note>
  `--audience-filter` and `--audience-filter-file` are two ways to supply the same ad-hoc filter, so they're mutually exclusive with each other — use `--audience-filter` for short inline JSON and `--audience-filter-file` to load it from a file. A filter can be combined with `--audience-segment-id`: when both are set, the filter is applied on top of the segment — contacts must match both. Setting a segment without a filter (or a filter without a segment) clears the other, so pass both together to keep both.
</Note>


# Completion
Source: https://loops.so/docs/cli/completion

Set up shell autocompletion for the Loops CLI.

The `completion` command generates autocompletion scripts for your shell.

## Bash

Requires the `bash-completion` package.

Load for the current session:

```bash theme={"dark"}
source <(loops completion bash)
```

Load for every new session:

```bash theme={"dark"}
# Linux
loops completion bash > /etc/bash_completion.d/loops

# macOS
loops completion bash > $(brew --prefix)/etc/bash_completion.d/loops
```

## Zsh

If shell completion is not already enabled, run once:

```bash theme={"dark"}
echo "autoload -U compinit; compinit" >> ~/.zshrc
```

Load for the current session:

```bash theme={"dark"}
source <(loops completion zsh)
```

Load for every new session:

```bash theme={"dark"}
# Linux
loops completion zsh > "${fpath[1]}/_loops"

# macOS
loops completion zsh > $(brew --prefix)/share/zsh/site-functions/_loops
```

## Fish

Load for the current session:

```bash theme={"dark"}
loops completion fish | source
```

Load for every new session:

```bash theme={"dark"}
loops completion fish > ~/.config/fish/completions/loops.fish
```

## PowerShell

Load for the current session:

```powershell theme={"dark"}
loops completion powershell | Out-String | Invoke-Expression
```

To load for every new session, add the output of the above command to your PowerShell profile.


# Components
Source: https://loops.so/docs/cli/components

View, create, and update email components.

The `components` command lets you view, create, and update [components](/docs/creating-emails/components) saved in your Loops account.

## `create`

Create a new component. Pass [LMX](/docs/creating-emails/lmx) inline with `--lmx` or from a file with `--lmx-file`:

```bash theme={"dark"}
loops components create --name "Header" --lmx "<mj-text>Hello</mj-text>"
```

```bash theme={"dark"}
loops components create --name "Header" --lmx-file ./header.lmx
```

### Flags

| Flag         | Description                                                                                    |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `--name`     | Component name <Badge>Required</Badge>                                                         |
| `--lmx`      | [LMX](/docs/creating-emails/lmx) markup (inline). Mutually exclusive with `--lmx-file`.             |
| `--lmx-file` | Path to a file containing [LMX](/docs/creating-emails/lmx) markup. Mutually exclusive with `--lmx`. |

## `get`

Get a component by ID.

```bash theme={"dark"}
loops components get cmn5ziajr01oztzlifwarda8m
```

## `list`

List components.

```bash theme={"dark"}
loops components list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected component ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops components list --pick
```

### Flags

| Flag         | Description                                                                                                       |
| ------------ | ----------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a component and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                              |
| `--cursor`   | Pagination cursor for a specific page                                                                             |

## `update`

Update a component.

```bash theme={"dark"}
loops components update cmn5ziajr01oztzlifwarda8m --name "Header" --lmx-file ./header.lmx
```

### Flags

| Flag         | Description                                                                                    |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `--name`     | Component name                                                                                 |
| `--lmx`      | [LMX](/docs/creating-emails/lmx) markup (inline). Mutually exclusive with `--lmx-file`.             |
| `--lmx-file` | Path to a file containing [LMX](/docs/creating-emails/lmx) markup. Mutually exclusive with `--lmx`. |


# Contact properties
Source: https://loops.so/docs/cli/contact-properties

Create and list contact properties.

The `contact-properties` command lets you manage contact properties in your Loops account.

## `create`

Create a new contact property.

```bash theme={"dark"}
loops contact-properties create \
  --name planName \
  --type string
```

### Flags

| Flag     | Description                                                                     |
| -------- | ------------------------------------------------------------------------------- |
| `--name` | Property name (camelCase, e.g. `planName`) <Badge>Required</Badge>              |
| `--type` | Property type (`string`, `number`, `boolean` or `date`) <Badge>Required</Badge> |

<Note>
  There are a few [reserved names](/docs/contacts/properties#reserved-names) that you
  cannot use for contact properties.
</Note>

## `list`

List all contact properties.

```bash theme={"dark"}
loops contact-properties list
```

To show only custom properties:

```bash theme={"dark"}
loops contact-properties list --custom
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected property to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops contact-properties list --pick
```

### Flags

| Flag       | Description                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| `--custom` | Only list custom properties                                                                                  |
| `--pick`   | Interactively pick a property and copy it to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |


# Contacts
Source: https://loops.so/docs/cli/contacts

Create, update, find, and delete contacts, and manage suppression.

The `contacts` command lets you manage contacts in your Loops audience.

## `create`

Create a new contact.

```bash theme={"dark"}
loops contacts create \
  --email test@example.com \
  --first-name Alice \
  --list "cm06f5v0e45nf0ml5754o9cix=true"
```

### Flags

| Flag              | Short | Description                                                                                                                                                                                                                      |
| ----------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--email`         | `-e`  | Contact email address <Badge>Required</Badge>                                                                                                                                                                                    |
| `--user-id`       | `-u`  | User ID                                                                                                                                                                                                                          |
| `--first-name`    |       | First name                                                                                                                                                                                                                       |
| `--last-name`     |       | Last name                                                                                                                                                                                                                        |
| `--subscribed`    | `-s`  | [Subscribed status](/docs/contacts/properties#subscribed) (`true` or `false`). We recommend leaving this field out unless you specifically want to unsubscribe or re-subscribe a contact. All new contacts are subscribed by default. |
| `--user-group`    |       | User group                                                                                                                                                                                                                       |
| `--source`        |       | Source ("API" by default)                                                                                                                                                                                                        |
| `--list`          |       | Mailing list subscription as `id=true\|false` (repeatable)                                                                                                                                                                       |
| `--prop`          |       | Contact property as `KEY=value` (repeatable)                                                                                                                                                                                     |
| `--contact-props` |       | Path to a JSON file of [contact properties](/docs/contacts/properties)                                                                                                                                                                |

### Contact properties

Use `--prop` to set [contact properties](/docs/contacts/properties) inline:

```bash theme={"dark"}
loops contacts create \
  --email test@example.com \
  --first-name Alice \
  --prop "planName=Pro" \
  --prop "signupDate=2025-01-15"
```

Alternatively, use `--contact-props` to set properties from a JSON file:

```json props.json theme={"dark"}
{
  "firstName": "Alice",
  "planName": "Pro",
  "signupDate": "2025-01-15"
}
```

```bash theme={"dark"}
loops contacts create \
  --email test@example.com \
  --contact-props ./props.json
```

<Note>
  The flags for default properties (i.e. `--first-name` or `--subscribed`) take precedence over the `--prop` flag.
</Note>

## `update`

Update an existing contact. Identify the contact by email or user ID.

```bash theme={"dark"}
loops contacts update \
  --email test@example.com \
  --last-name Smith \
  --list "cm06f5v0e45nf0ml5754o9cix=true"
```

### Flags

| Flag              | Short | Description                                                              |
| ----------------- | ----- | ------------------------------------------------------------------------ |
| `--email`         | `-e`  | Contact email address                                                    |
| `--user-id`       | `-u`  | User ID                                                                  |
| `--first-name`    |       | First name                                                               |
| `--last-name`     |       | Last name                                                                |
| `--subscribed`    | `-s`  | [Subscribed status](/docs/contacts/properties#subscribed) (`true` or `false`) |
| `--user-group`    |       | User group                                                               |
| `--list`          |       | Mailing list subscription as `id=true\|false` (repeatable)               |
| `--prop`          |       | Contact property as `KEY=value` (repeatable)                             |
| `--contact-props` |       | Path to a JSON file of contact properties                                |

### Contact properties

Use `--prop` to set [contact properties](/docs/contacts/properties) inline:

```bash theme={"dark"}
loops contacts update \
  --email test@example.com \
  --last-name Smith \
  --prop "planName=Unlimited"
```

Alternatively, use `--contact-props` to set properties from a JSON file:

```json props.json theme={"dark"}
{
  "lastName": "Smith",
  "planName": "Unlimited",
}
```

```bash theme={"dark"}
loops contacts update \
  --email test@example.com \
  --contact-props ./props.json
```

<Note>
  The flags for default properties (i.e. `--first-name` or `--subscribed`) take precedence over the `--prop` flag.
</Note>

## `find`

Find a contact by email or user ID.

```bash theme={"dark"}
loops contacts find --email test@example.com
```

### Flags

| Flag        | Short | Description           |
| ----------- | ----- | --------------------- |
| `--email`   | `-e`  | Contact email address |
| `--user-id` | `-u`  | Contact user ID       |

## `delete`

Delete a contact by email or user ID.

```bash theme={"dark"}
loops contacts delete --email test@example.com
```

### Flags

| Flag        | Short | Description           |
| ----------- | ----- | --------------------- |
| `--email`   | `-e`  | Contact email address |
| `--user-id` | `-u`  | Contact user ID       |

## `suppression check`

Check whether a contact is on the suppression list. Identify the contact by email or user ID.

```bash theme={"dark"}
loops contacts suppression check --email test@example.com
```

### Flags

| Flag        | Short | Description           |
| ----------- | ----- | --------------------- |
| `--email`   | `-e`  | Contact email address |
| `--user-id` | `-u`  | Contact user ID       |

## `suppression remove`

Remove a contact from the suppression list. Identify the contact by email or user ID.

```bash theme={"dark"}
loops contacts suppression remove --email test@example.com
```

### Flags

| Flag        | Short | Description           |
| ----------- | ----- | --------------------- |
| `--email`   | `-e`  | Contact email address |
| `--user-id` | `-u`  | Contact user ID       |


# Email messages
Source: https://loops.so/docs/cli/email-messages

Get and update draft email messages.

The `email-messages` command lets you view and edit the email message attached to a campaign. Email messages are created automatically when you [create a campaign](/docs/cli/campaigns#create).

Use this command when you want to keep email content in Git as `.lmx` files,
review changes locally, and push updates to a draft email message with revision
protection.

## `get`

Get an email message by ID, including its current content and `contentRevisionId`.

```bash theme={"dark"}
loops email-messages get cmn5zia4i0017tzli8ric8giv
```

## `guardian`

Run [Guardian](/docs/creating-emails/guardian) content validation on an email message. See [Run Guardian checks](/docs/api-reference/run-guardian-checks) for the checks performed and the response format.

```bash theme={"dark"}
loops email-messages guardian cmn5zia4i0017tzli8ric8giv
```

## `preview`

Send a test preview of an email message to the addresses passed in `--email`. The variable fields accepted depend on the parent type:

* Campaign previews accept `--contact-prop`.
* Workflow previews accept `--contact-prop` and `--event-prop`.
* Transactional previews accept `--var` and `--json-vars`.

```bash theme={"dark"}
loops email-messages preview cmn5zia4i0017tzli8ric8giv \
  --email test@example.com \
  --email another@example.com
```

### Flags

| Flag             | Short | Description                                                                         |
| ---------------- | ----- | ----------------------------------------------------------------------------------- |
| `--email`        |       | Recipient email address (repeatable; at least one required) <Badge>Required</Badge> |
| `--contact-prop` |       | Contact property as `KEY=value` (repeatable)                                        |
| `--event-prop`   |       | Event property as `KEY=value` (repeatable)                                          |
| `--var`          | `-v`  | Data variable as `KEY=value` (repeatable; transactional previews only)              |
| `--json-vars`    | `-j`  | Path to a JSON file of data variables                                               |

## `update`

Update fields on a draft email message. The owning campaign must be in `Draft` status.

```bash theme={"dark"}
loops email-messages update cmn5zia4i0017tzli8ric8giv \
  --expected-revision-id clv8g2x4z012yl70n5o6p7q8r \
  --subject "Welcome to Acme" \
  --preview-text "Here's how to get started" \
  --lmx-file ./email.lmx
```

### Concurrency control

Every update must include either `--expected-revision-id` or `--force`:

* `--expected-revision-id` — pass the `contentRevisionId` from a prior `email-messages get`. The request is rejected with a conflict if the server's revision has advanced, so you don't overwrite a concurrent edit.
* `--force` — fetch the current revision automatically and use it. This overwrites any concurrent edits.

### Setting content

Pass [LMX](/docs/creating-emails/lmx) inline with `--lmx` or from a file with `--lmx-file`:

```bash theme={"dark"}
loops email-messages update cmn5zia4i0017tzli8ric8giv \
  --force \
  --lmx-file ./email.lmx
```

### Git workflow

For a safer file-based workflow:

1. Run `loops email-messages get <email-message-id>` and save the current LMX
   in your repository.
2. Commit the `.lmx` file or review it in a branch.
3. Update the draft email with `--lmx-file` and the latest
   `--expected-revision-id`.
4. Save the returned `contentRevisionId` before making another update.

Use `--force` only when you intentionally want to overwrite the latest draft
content.

### Flags

| Flag                     | Short | Description                                                                                                            |
| ------------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------- |
| `--expected-revision-id` | `-r`  | Last-seen `contentRevisionId` from `email-messages get`. Mutually exclusive with `--force`.                            |
| `--force`                | `-f`  | Fetch the current revision and use it (overwrites concurrent edits). Mutually exclusive with `--expected-revision-id`. |
| `--subject`              |       | Email subject                                                                                                          |
| `--preview-text`         |       | Email preview text                                                                                                     |
| `--from-name`            |       | Sender display name                                                                                                    |
| `--from-email`           |       | Sender username (the part before `@` — the team's sending domain is appended automatically)                            |
| `--reply-to`             |       | Reply-to email address                                                                                                 |
| `--cc`                   |       | CC email address                                                                                                       |
| `--bcc`                  |       | BCC email address                                                                                                      |
| `--email-format`         |       | Email format (`styled` or `plain`)                                                                                     |
| `--language-code`        |       | Language code, [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`, `fr`)                    |
| `--lmx`                  |       | [LMX](/docs/creating-emails/lmx) markup (inline). Mutually exclusive with `--lmx-file`.                                     |
| `--lmx-file`             |       | Path to a file containing [LMX](/docs/creating-emails/lmx) markup. Mutually exclusive with `--lmx`.                         |
| `--contact-fallback`     |       | Contact property fallback as `KEY=value` (repeatable). Replaces all server-side contact fallbacks.                     |
| `--data-fallback`        |       | Transactional data-variable fallback as `KEY=value` (repeatable). Replaces all server-side data fallbacks.             |
| `--event-fallback`       |       | Event property fallback as `KEY=value` (repeatable). Replaces all server-side event fallbacks.                         |

At least one content flag (`--subject`, `--preview-text`, `--from-name`, `--from-email`, `--reply-to`, `--lmx`, or `--lmx-file`) must be provided.


# Event patterns
Source: https://loops.so/docs/cli/event-patterns

Read event patterns detected in your account.

The `event-patterns` command reads event patterns detected in your account from the [events](/docs/cli/events) you send (v0.9.0).

## `list`

List event patterns.

```bash theme={"dark"}
loops event-patterns list
```

### Flags

| Flag         | Description                           |
| ------------ | ------------------------------------- |
| `--per-page` | Results per page (10–50, default 20)  |
| `--cursor`   | Pagination cursor for a specific page |

## `get`

Get an event pattern by ID or name.

```bash theme={"dark"}
loops event-patterns get --name signup_completed
```

```bash theme={"dark"}
loops event-patterns get --id cmnx1d95z001llilji4vapgqs
```

### Flags

| Flag     | Description      |
| -------- | ---------------- |
| `--id`   | Event pattern ID |
| `--name` | Event name       |


# Events
Source: https://loops.so/docs/cli/events

Send events to trigger automations.

The `events` command lets you send events to Loops to trigger automations.

## `send`

Send an event for a contact identified by email or user ID.

```
loops events send <event> [--flags]
```

Example:

```bash theme={"dark"}
loops events send signup \
  --email test@example.com
```

### Flags

| Flag                | Description                                                       |
| ------------------- | ----------------------------------------------------------------- |
| `--email`           | Contact email address (required if `--user-id` is not provided)   |
| `--user-id`         | Contact user ID (required if `--email` is not provided)           |
| `--props`           | Path to a JSON file of [event properties](/docs/events/properties)     |
| `--contact-props`   | Path to a JSON file of [contact properties](/docs/contacts/properties) |
| `--list`            | Mailing list subscription as `id=true\|false` (repeatable)        |
| `--idempotency-key` | Idempotency key to prevent duplicate sends                        |

### Event properties file

Use `--props` to attach [event properties](/docs/events/properties) to the event:

```json event-props.json theme={"dark"}
{
  "plan": "Pro",
  "trialDays": 14
}
```

```bash theme={"dark"}
loops events send signup \
  --email test@example.com \
  --props ./event-props.json
```


# Lists
Source: https://loops.so/docs/cli/lists

View your mailing lists.

The `lists` command lets you view mailing lists in your Loops account.

## `list`

List all mailing lists.

```bash theme={"dark"}
loops lists list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected mailing list ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops lists list --pick
```

### Flags

| Flag     | Description                                                                                                          |
| -------- | -------------------------------------------------------------------------------------------------------------------- |
| `--pick` | Interactively pick a mailing list and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |


# Themes
Source: https://loops.so/docs/cli/themes

View, create, and update email themes.

The `themes` command lets you view, create, and update [themes](/docs/creating-emails/styles) saved in your Loops account.

## `create`

Create a new theme. Pass theme styles as a JSON file with `--styles-file`.

```json styles.json theme={"dark"}
{
  "backgroundColor": "#ffffff",
  "bodyColor": "#f5f5f5",
  "textBaseColor": "#1a1a1a",
  "textLinkColor": "#2563eb",
  "buttonBodyColor": "#2563eb",
  "buttonTextColor": "#ffffff",
  "borderRadius": 8
}
```

```bash theme={"dark"}
loops themes create --name "Newsletter" --styles-file ./styles.json
```

### Flags

| Flag            | Short | Description                           |
| --------------- | ----- | ------------------------------------- |
| `--name`        | `-n`  | Theme name <Badge>Required</Badge>    |
| `--styles-file` |       | Path to a JSON file with theme styles |

## `get`

Get a theme by ID.

```bash theme={"dark"}
loops themes get clo1z5q7s004yl70y3z4a5b6c
```

## `list`

List themes.

```bash theme={"dark"}
loops themes list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected theme ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops themes list --pick
```

### Flags

| Flag         | Description                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a theme and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                          |
| `--cursor`   | Pagination cursor for a specific page                                                                         |

## `update`

Update a theme.

```bash theme={"dark"}
loops themes update clo1z5q7s004yl70y3z4a5b6c --name "Newsletter (dark)" --styles-file ./styles.json
```

### Flags

| Flag            | Short | Description                           |
| --------------- | ----- | ------------------------------------- |
| `--name`        | `-n`  | Theme name                            |
| `--styles-file` |       | Path to a JSON file with theme styles |


# Transactional
Source: https://loops.so/docs/cli/transactional

Send and manage transactional emails.

The `transactional` command lets you send and manage transactional emails. There is also a top-level `loops send` shortcut for quickly sending a transactional email.

## `send`

Send a transactional email.

```
loops transactional send <id> [--flags]
```

Example:

```bash theme={"dark"}
loops transactional send cmnx1d95z001llilji4vapgqs \
  --email test@example.com
```

Pass [data variables](/docs/transactional#data-variables) inline with `--var` or from a JSON file with `--json-vars`:

```bash theme={"dark"}
loops transactional send cmnx1d95z001llilji4vapgqs \
  --email test@example.com \
  --var name=Alice \
  --var company=Acme
```

```bash theme={"dark"}
loops transactional send cmnx1d95z001llilji4vapgqs \
  --email test@example.com \
  --json-vars ./vars.json
```

Add attachments with `--attachment`:

```bash theme={"dark"}
loops transactional send cmnx1d95z001llilji4vapgqs \
  --email test@example.com \
  --attachment ./attachment.pdf
```

### Flags

| Flag                | Short | Description                                            |
| ------------------- | ----- | ------------------------------------------------------ |
| `--email`           |       | Recipient email address <Badge>Required</Badge>        |
| `--var`             | `-v`  | Data variable as `KEY=value` (repeatable)              |
| `--json-vars`       | `-j`  | Path to a JSON file of data variables                  |
| `--attachment`      | `-A`  | Path to a file to attach (repeatable)                  |
| `--add-to-audience` | `-a`  | Create a contact in your audience if one doesn't exist |
| `--idempotency-key` |       | Idempotency key to prevent duplicate sends             |

## `create`

Create a transactional email with an empty draft.

```bash theme={"dark"}
loops transactional create --name "Password reset"
```

Assign the email to a [transactional group](/docs/cli/transactional-groups) with `--transactional-group-id`:

```bash theme={"dark"}
loops transactional create \
  --name "Password reset" \
  --transactional-group-id cmqz4t8b0002mkabc9wxyz123
```

### Flags

| Flag                       | Short | Description                                      |
| -------------------------- | ----- | ------------------------------------------------ |
| `--name`                   | `-n`  | Transactional email name <Badge>Required</Badge> |
| `--transactional-group-id` |       | Transactional group ID to assign this email to   |

## `get`

Get a transactional email.

```bash theme={"dark"}
loops transactional get cmnx1d95z001llilji4vapgqs
```

## `draft`

Ensure a draft exists for a transactional email. If a draft already exists it is returned unchanged; otherwise a new empty draft is created (seeded from the most recent published version when present).

```bash theme={"dark"}
loops transactional draft cmnx1d95z001llilji4vapgqs
```

## `publish`

Publish the current draft of a transactional email.

```bash theme={"dark"}
loops transactional publish cmnx1d95z001llilji4vapgqs
```

## `update`

Update a transactional email's name or group.

To update the email's content, edit its related [email message](/docs/cli/email-messages).

```bash theme={"dark"}
loops transactional update cmnx1d95z001llilji4vapgqs \
  --name "Password reset email"
```

Move the email to another [transactional group](/docs/cli/transactional-groups) with `--transactional-group-id`:

```bash theme={"dark"}
loops transactional update cmnx1d95z001llilji4vapgqs \
  --transactional-group-id cmqz4t8b0002mkabc9wxyz123
```

### Flags

| Flag                       | Short | Description                                      |
| -------------------------- | ----- | ------------------------------------------------ |
| `--name`                   | `-n`  | Transactional email name <Badge>Required</Badge> |
| `--transactional-group-id` |       | Move the email to this transactional group       |

## `list`

List published transactional emails.

```bash theme={"dark"}
loops transactional list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected transactional ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops transactional list --pick
```

### Flags

| Flag         | Description                                                                                                                 |
| ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a transactional email and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                                        |
| `--cursor`   | Pagination cursor for a specific page                                                                                       |

## `loops send` shortcut

`loops send` is a top-level shortcut for `loops transactional send`. It accepts the same flags.

```
loops send <id> [--flags]
```

Example:

```bash theme={"dark"}
loops send cmnx1d95z001llilji4vapgqs \
  --email test@example.com \
  --var name=Alice
```


# Transactional groups
Source: https://loops.so/docs/cli/transactional-groups

Create, list, and update transactional groups.

The `transactional-groups` command lets you manage transactional groups, which are used to organize your [transactional emails](/docs/cli/transactional).

## `create`

Create a transactional group.

```bash theme={"dark"}
loops transactional-groups create --name "Account emails"
```

Add a description with `--description`:

```bash theme={"dark"}
loops transactional-groups create \
  --name "Account emails" \
  --description "Password resets, email verification, and login alerts"
```

### Flags

| Flag            | Short | Description                                      |
| --------------- | ----- | ------------------------------------------------ |
| `--name`        | `-n`  | Transactional group name <Badge>Required</Badge> |
| `--description` | `-d`  | Transactional group description                  |

## `get`

Get a transactional group by ID.

```bash theme={"dark"}
loops transactional-groups get cmqz4t8b0002mkabc9wxyz123
```

## `list`

List transactional groups.

```bash theme={"dark"}
loops transactional-groups list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected transactional group ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops transactional-groups list --pick
```

### Flags

| Flag         | Description                                                                                                                 |
| ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a transactional group and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                                        |
| `--cursor`   | Pagination cursor for a specific page                                                                                       |

## `update`

Update a transactional group.

```bash theme={"dark"}
loops transactional-groups update cmqz4t8b0002mkabc9wxyz123 \
  --name "Transactional emails"
```

### Flags

| Flag            | Short | Description     |
| --------------- | ----- | --------------- |
| `--name`        | `-n`  | New name        |
| `--description` | `-d`  | New description |


# Uploads
Source: https://loops.so/docs/cli/uploads

Upload files as email assets.

The `uploads` command lets you upload files as email assets that can be referenced in [LMX](/docs/creating-emails/lmx) email content.

## `create`

Upload a file as an email asset.

```bash theme={"dark"}
loops uploads create ./header.png
```

Use the returned `finalUrl` in [LMX `<Image />` elements](/docs/creating-emails/lmx#images).

By default the MIME type is sniffed from the file contents. Override it with `--content-type` if needed:

```bash theme={"dark"}
loops uploads create ./header.png --content-type image/png
```

### Flags

| Flag             | Description                                                           |
| ---------------- | --------------------------------------------------------------------- |
| `--content-type` | MIME type to use for the upload (default: sniffed from file contents) |


# Workflows
Source: https://loops.so/docs/cli/workflows

List, inspect, create, and update workflows and their nodes.

<Note>
  Workflow write commands are now available, but full support for the [Workflow API](/docs/api-reference/examples/workflows) is still evolving. Endpoints are in alpha and are subject to change.
</Note>

The `workflows` command lets you list, inspect, create, and update workflows and their nodes in your Loops account.

## `list`

List workflows.

```bash theme={"dark"}
loops workflows list
```

Pass `--pick` to filter the list interactively with [fzf](https://github.com/junegunn/fzf) and copy the selected workflow ID to your clipboard. Requires `fzf` to be installed and on your `PATH`.

```bash theme={"dark"}
loops workflows list --pick
```

### Flags

| Flag         | Description                                                                                                      |
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `--pick`     | Interactively pick a workflow and copy its ID to the clipboard. Requires [fzf](https://github.com/junegunn/fzf). |
| `--per-page` | Results per page (10–50, default 20)                                                                             |
| `--cursor`   | Pagination cursor for a specific page                                                                            |

## `get`

Get a workflow by ID. Returns workflow metadata and a summary table of its nodes. For full node configuration, use [`workflows nodes get`](#nodes-get).

```bash theme={"dark"}
loops workflows get cls5d9u1w009yl70c7d8e9f0g
```

## Optimistic concurrency

Workflow write commands that modify an existing workflow accept an `--expected-revision-id` flag to guard against overwriting concurrent changes. Pass the revision ID from a prior [`workflows get`](#get); if the workflow has changed since then, the request is rejected with a conflict.

## `create`

Create a workflow.

```
loops workflows create --name <name> [--flags]
```

```bash theme={"dark"}
loops workflows create \
  --name "Welcome series" \
  --description "Onboarding emails for new signups"
```

### Flags

| Flag                | Short | Description                              |
| ------------------- | ----- | ---------------------------------------- |
| `--name`            | `-n`  | Workflow name <Badge>Required</Badge>    |
| `--description`     | `-d`  | Workflow description                     |
| `--mailing-list-id` |       | Mailing list ID to send the workflow to. |

## `update`

Update a workflow's name and/or description.

```
loops workflows update <workflow-id> [--flags]
```

```bash theme={"dark"}
loops workflows update cls5d9u1w009yl70c7d8e9f0g \
  --name "Welcome series v2" \
  --expected-revision-id clv8g2x4z012yl70n5o6p7q8r
```

### Flags

| Flag                     | Short | Description                                                                          |
| ------------------------ | ----- | ------------------------------------------------------------------------------------ |
| `--name`                 | `-n`  | Workflow name                                                                        |
| `--description`          | `-d`  | Workflow description                                                                 |
| `--expected-revision-id` |       | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency). |

## `change-mailing-list`

Change a workflow's mailing list.

```
loops workflows change-mailing-list <workflow-id> --mailing-list-id <mailing-list-id> [--flags]
```

```bash theme={"dark"}
loops workflows change-mailing-list cls5d9u1w009yl70c7d8e9f0g \
  --mailing-list-id clw9h3y5a013yl70o6p7q8r9s
```

Preview the impact on queued contacts before applying the change with `--dry-run`.

### Flags

| Flag                      | Description                                                                          |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `--mailing-list-id`       | Mailing list ID. Pass `"null"` to clear. <Badge>Required</Badge>                     |
| `--dry-run`               | Report queued-contact impact without applying the change.                            |
| `--queued-contact-policy` | How to treat queued contacts: `fail` or `discard`.                                   |
| `--expected-revision-id`  | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency). |

## `nodes get`

Get full details for a single workflow node. See [Get workflow node](/docs/api-reference/get-workflow-node) for the full response schema.

```bash theme={"dark"}
loops workflows nodes get <workflow-id> <node-id>
loops workflows nodes get cls5d9u1w009yl70c7d8e9f0g clt6e0v2x010yl70h1i2j3k4l
```

For complex node configuration (such as audience filters or event property queries), use `--output json`.

## `nodes create`

Create a workflow node.

```
loops workflows nodes create <workflow-id> --insert-mode <mode> --node-type <type> [--flags]
```

```bash theme={"dark"}
loops workflows nodes create cls5d9u1w009yl70c7d8e9f0g \
  --insert-mode before \
  --node-type SendEmailAction \
  --to-node-id clt6e0v2x010yl70h1i2j3k4l
```

Choose where the node is inserted with `--insert-mode`:

* `between` — insert between two existing nodes, using `--from-node-id` and `--to-node-id`.
* `before` — insert ahead of a node, using `--to-node-id`.
* `after` — insert after `--from-node-id`. The source node must have exactly one outgoing connection.

### Flags

| Flag                     | Description                                                                                                                                    |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `--insert-mode`          | Insert mode: `between`, `before`, or `after`. <Badge>Required</Badge>                                                                          |
| `--node-type`            | Node type: `AudienceFilter`, `BranchNode`, `ExperimentBranchNode`, `TimerAction`, `SendEmailAction`, or `VariantNode`. <Badge>Required</Badge> |
| `--from-node-id`         | Source node ID (insert-mode `between` or `after`).                                                                                             |
| `--to-node-id`           | Target node ID (insert-mode `between` or `before`).                                                                                            |
| `--expected-revision-id` | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency).                                                           |

## `nodes update`

Update a workflow node.

```
loops workflows nodes update <workflow-id> <node-id> --payload-file <path> [--flags]
```

```bash theme={"dark"}
loops workflows nodes update cls5d9u1w009yl70c7d8e9f0g clt6e0v2x010yl70h1i2j3k4l \
  --payload-file ./node.json \
  --expected-revision-id clv8g2x4z012yl70n5o6p7q8r
```

The payload file must contain a `typeName` field identifying the node type:

```json theme={"dark"}
{
  "typeName": "TimerAction"
}
```

See [Get workflow node](/docs/api-reference/get-workflow-node) for the full node schema.

### Flags

| Flag                     | Description                                                                                          |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `--payload-file`         | Path to a JSON file with the node payload (must include a `typeName` field). <Badge>Required</Badge> |
| `--expected-revision-id` | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency).                 |

## `nodes delete`

Delete a workflow node.

```
loops workflows nodes delete <workflow-id> <node-id> [--flags]
```

```bash theme={"dark"}
loops workflows nodes delete cls5d9u1w009yl70c7d8e9f0g clt6e0v2x010yl70h1i2j3k4l \
  --recursive
```

Preview the impact on queued contacts before applying the deletion with `--dry-run`.

### Flags

| Flag                      | Description                                                                          |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `--recursive`             | Also delete downstream nodes.                                                        |
| `--dry-run`               | Report queued-contact impact without applying the deletion.                          |
| `--queued-contact-policy` | How to treat queued contacts: `fail` or `discard`.                                   |
| `--expected-revision-id`  | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency). |

## `nodes add-branch`

Add a branch to a branch node.

```
loops workflows nodes add-branch <workflow-id> <node-id> [--flags]
```

```bash theme={"dark"}
loops workflows nodes add-branch cls5d9u1w009yl70c7d8e9f0g clt6e0v2x010yl70h1i2j3k4l
```

### Flags

| Flag                     | Description                                                                          |
| ------------------------ | ------------------------------------------------------------------------------------ |
| `--expected-revision-id` | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency). |

## `nodes reroute`

Move a node's single outgoing connection to a new target. The source node must have exactly one outgoing connection.

```
loops workflows nodes reroute <workflow-id> <node-id> --new-target-node-id <node-id> [--flags]
```

```bash theme={"dark"}
loops workflows nodes reroute cls5d9u1w009yl70c7d8e9f0g clt6e0v2x010yl70h1i2j3k4l \
  --new-target-node-id clw9h3y5a013yl70o6p7q8r9s
```

### Flags

| Flag                     | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `--new-target-node-id`   | Node ID that should receive the source node's outgoing connection. <Badge>Required</Badge> |
| `--expected-revision-id` | Expected workflow revision ID for [optimistic concurrency](#optimistic-concurrency).       |


# Contact activity timeline
Source: https://loops.so/docs/contacts/contact-activity

The contact activity timeline is a great way to see all the activity for a specific contact.

<img alt="contact property timeline" />

The contact activity timeline provides a chronological view of all the activity for a specific contact. To access the contact activity timeline, click on a contact from inside an audience.
Email properties that can show on the timeline include:

* Sent
* Opened
* Clicked
* Soft bounced
* Hard bounced
* Marked as spam
* Unsubscribed

Additionally, we also show the following properties:

* Added to audience
* Event fired


# Delete contacts
Source: https://loops.so/docs/contacts/delete-contacts

Remove contacts from your audience.

## Delete single contacts

You can delete contacts from your audience on the [Audience page](https://app.loops.so/audience).

Click the `•••` menu icon on the contact you want to delete and select **Delete**.

<img alt="Deleting a contact" />

## Delete groups of contacts

To delete contacts in bulk, use the filters on the [Audience page](https://app.loops.so/audience) to narrow down the selection of contacts you want to delete.

Then click the `•••` menu icon in the filter box at the top of the Audience page and select **Delete contacts**.

This button will delete all contacts listed in the table below, based on the filter(s) you set up.

<img alt="Deleting contacts in bulk using filters" />

## Delete contacts with the API

You can delete single contacts using the API's [Delete contact](/docs/api-reference/delete-contact) endpoint, by email address or `userId` value.

```json theme={"dark"}
POST https://app.loops.so/api/v1/contacts/delete

{
  "email": "test@example.com"
}
```


# Double opt-in
Source: https://loops.so/docs/contacts/double-opt-in

Require and manage subscription confirmations for new contacts.

Double opt-in requires new contacts to confirm their subscription before you can send them marketing emails. This improves list quality and deliverability.

## Good to know

This feature only applies to [marketing sends](/docs/types-of-emails) (campaigns and workflows). Transactional emails are never restricted by double opt-in.

Double opt-in is currently only enabled on [Form endpoints](/docs/forms/simple-form). API endpoints like [Create contact](/docs/api-reference/create-contact) and [Update contact](/docs/api-reference/update-contact) are not yet gated. Coverage will expand to these endpoints soon.

## The double opt-in flow

When double opt-in is enabled:

1. Contact submits a form and receives a confirmation email with a link to a branded confirmation page.
2. The confirmation page displays **Confirm subscription** and **No thanks** buttons.
3. Contacts appear in your audience as "Pending" in the **Double opt-in** column.
4. Contact confirms or rejects the subscription:
   1. If they click **Confirm subscription**, they're subscribed to your audience and any selected mailing lists. This also triggers any applicable workflows.
   2. If they click **No thanks**, they remain in your audience but are marked as [unsubscribed](/docs/contacts/properties#subscribed).\
      They can request a new confirmation email from the same page if they clicked **No thanks** by accident.
      <Tip>
        Unsubscribed contacts do not count toward [your plan's
        limits](https://loops.so/pricing).
      </Tip>
   3. If they don't respond, the contact continues to appear as "Pending". Pending contacts are not automatically removed. You can manually delete them if needed.

<img alt="Double opt-in confirmation page" />

## Enable double opt-in

To enable double opt-in, go to [Settings -> Sending](https://app.loops.so/settings?page=sending) and scroll to the **Double opt-in** section. Turn the **Double opt-in** setting on.

This creates a special transactional email that is used to confirm the subscription, which you can customize in the [Transactional](https://app.loops.so/transactional) page in the Loops dashboard.

To disable double opt-in, turn the **Double opt-in** setting off.

<img alt="Double opt-in settings" />

## Customize the confirmation email

The opt-in email is a special [transactional email](/docs/transactional) that is created automatically when you first turn on double opt-in in your account.

You are free to customize the email as you like but it has specific requirements:

* Keep the email short, clearly branded, and focused on the confirmation action.
* Include the required data variable `optInUrl` (added automatically when the email is created).
* Other data variables are not allowed.

Click **Edit Draft** to [edit the email](/docs/transactional#editing-the-email). Make sure to click **Publish** after editing to make your changes live.

<CardGroup>
  <Card title="Transactional email guide" icon="code" href="/transactional">
    Learn how to create and send transactional email with Loops.
  </Card>
</CardGroup>

## Manually re-send a confirmation email

You can re-send a confirmation email to a contact from their profile page. This is only available for contacts who are currently "Pending".

On a contact's profile page, click the `•••` menu icon and choose **Request opt-in**.

<img alt="Request opt-in" />

## Webhooks and the API

When double opt-in is enabled, contact webhooks don't fire until the contact is confirmed. Specifically:

* The [`contact.created` event](/docs/webhooks#event-types) will only be sent for contacts created via forms once the contact has confirmed their subscription.
* Other contact-related webhooks (such as `contact.mailingList.subscribed`) will also only fire after confirmation.
* Contacts remain in a "Pending" state in your audience until they confirm, and no webhook events are triggered during this pending state.

You can read a contact's opt-in status from the API by looking at the `optInStatus` field in the [Find contact](/docs/api-reference/find-contact) endpoint.


# Email blocklist
Source: https://loops.so/docs/contacts/email-blocklist

Stop certain email addresses or domains from being added to your audience.

You can stop certain emails from being added to your audience by using Loops' Email Blocklist feature.

Go to [Settings -> Account](https://app.loops.so/settings?page=account) and click **Add pattern**.

In the input, enter an email pattern to block.

For example, you can input `*@example.com` to block all email addresses ending with `@example.com`, or `*@*.edu` to block all `.edu` email addresses.

To block an individual email address, simply insert the full address like `test@example.com`.

The blocklist feature works across all methods that allow signups to your audience: manually adding in Loops, CSV uploads, forms, the API and all integrations.


# Export contacts
Source: https://loops.so/docs/contacts/export-contacts

Download CSVs of your contact data.

At any point you can download a CSV file of your audience. This can be your whole audience, or a specific subset of contacts defined by [filters or segments](/docs/contacts/filters-segments).

## Export your audience

To download a CSV of your audience, click the `•••` menu icon in the top right of the filter box and select **Download**.

<img alt="Exporting your audience" />

The CSV file will contain columns for each property of your contacts, including [default properties](/docs/contacts/properties#default-contact-properties) plus any [custom properties](/docs/contacts/properties#custom-contact-properties) you've added.

## Using filters and segments

If you want to only export a particular list of contacts or subset of your audience, you can use filters and segments.

Some example exports you can create:

* contacts who have opened a specific campaign
* contacts who were not sent one of your campaigns
* contacts who have clicked a link in one of your workflow emails
* contacts who subscribed after a certain date
* contacts who have a certain domain name in their email address

You can also combine filters to create more complex exports.

Here's an example of a filter you can use to export all contacts who have opened a specific campaign:

<img alt="Exporting with a filter" />

If you want to save your filters for the future, you can create a [segment](/docs/contacts/filters-segments#audience-segments) by clicking **Save segment**.

<img alt="Create a segment" />

For reusable groups like "opened this campaign" or "not sent this campaign",
save the filtered audience as a segment instead of exporting a one-off CSV.


# Filters and Segments
Source: https://loops.so/docs/contacts/filters-segments

How to send emails to specific groups of contacts and save segments for future use.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

After you have added contacts to your audience in Loops, you may want to create workflows or send campaigns to certain groups of contacts.

You can do this with filters and segments.

<img alt="Workflow" />

## Audience filters

When sending [campaigns](/docs/types-of-emails#campaigns) or emails inside [workflows](/docs/workflows), you can send to specific groups of contacts in your audience.

Choosing these contacts is done using filters, which are based on two sets of data:

* [contact properties](/docs/contacts/properties) (either default properties or custom properties you've added to your audience)
* **contact activity** from your campaigns and workflows (sends, not sent
  status, opens and clicks)

For example, you can use filters to build segments like:

* contacts with a specific domain in their email address
* contacts who were added via a specific source
* contacts who were sent a specific campaign
* contacts who were not sent a specific campaign
* contacts who opened or clicked a specific campaign or workflow email
* contacts who didn't open a specific email within one of your workflows

<img alt="Audience filter form" />

You can add multiple filters at once, and you can also choose to apply *all filters* ("All of these are true") or *any filter* ("Any of these are true").

Audience filters are automatically updated as contact properties are changed. This means that your emails will only send to contacts that match the filter at the time the email is sent.

## Activity filters

Activity filters let you segment contacts based on campaign and workflow email
activity:

* **Sent**: contacts who were sent a campaign or workflow email.
* **Not sent**: contacts who were not sent a selected campaign or workflow
  email.
* **Opened**: contacts who opened a campaign or workflow email.
* **Clicked**: contacts who clicked a tracked link in a campaign or workflow
  email.

For example, you can use **Not sent** to build a segment of contacts who have
not received a selected campaign yet, then save that filtered audience as a
segment.

If you want to measure whether a campaign drove a business outcome, use
[goals](/docs/goals/overview) instead of treating opens or clicks as the primary
engagement signal.

<Note>
  Transactional emails do not track opens or link clicks. Activity filters for
  opens and clicks apply to campaign and workflow emails.
</Note>

## Audience segments

Segments are just filters that are saved in your Loops account for the future. This makes them easily reusable between different workflows and campaigns.

<Tip>
  You can create segments while sending campaigns or workflows, or directly on the [Audience page](https://app.loops.so/audience).
</Tip>

To create a segment, first filter the audience table and then click **Save segment**.

<img alt="Create a segment" />

You will be prompted to add a name for your segment, then click **Finish**.

Your new segment will then be available in a dropdown whenever you filter your audience for campaigns or workflows.

<img alt="List of segments" />

Just like filters, segments are automatically updated when contact data is changed, meaning emails always send to the correct contacts at the time the email is sent.


# List management
Source: https://loops.so/docs/contacts/mailing-lists

Organize contacts and offer a subscriber preference center with mailing lists.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

Mailing lists are useful when you want to organize contacts in more public groups and when you want contacts to be able to opt in and out of specific lists.

Using Lists will automatically generate a branded Preference Center for contacts in your audience, allowing them to easily manage their subscription preferences.

Lists can be public or private and contacts can belong to many, one, or no lists.

For organizing contacts for your own use, we suggest using [filters and segments](/docs/contacts/filters-segments) instead.

<Tip>
  Lists are an optional feature. You can use Loops without lists but if you'd
  like finer-grained control over your contacts and the types of communication
  they receive, lists are available on any plan tier.
</Tip>

## List types

Each list you create can be **Public** or **Private**.

By default lists are private, meaning they are only shown to their subscribed contacts (non-subscribers won't be able to see or subscribe to private lists in the Preference Center).

If you want to allow general opt-in to a list, you can set the list visibility to `Public`. Public lists will be shown to all contacts in the Preference Center.

You can also sign up new subscribers to public lists with [Forms](/docs/forms/simple-form).

<Note>
  Both private and public lists are visible within your Loops admin and can be
  used for filtering contacts when sending campaigns and workflows.
</Note>

## Preference Center

The Preference Center allows your contacts to manage their own subscription preferences.

<img alt="Preference Center" />

A link to the contact-specific Preference Center is automatically added to each marketing email sent from Loops. You can link to the Preference Center in MJML emails by using the `{unsubscribe_link}` [dynamic tag](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax).

You can upload a company icon to brand your Preference Center. This option is shown just below your mailing lists in the [Lists settings page](https://app.loops.so/settings?page=lists).

<Tip>
  You can brand your unsubscribe page with a company icon even if you do not use
  the lists features.
</Tip>

Within the Preference Center, contacts will see:

* your company icon (if uploaded)
* the names and descriptions of all public lists
* the names and descriptions of all private lists they are subscribed to
* the option to unsubscribe from each list they are subscribed to

## Email footers

When sending campaigns and workflows to specific lists, the [email footer](/docs/creating-emails/editor#footer-content) will include the name of the list the email was sent to. This is useful for contacts to see which list the email was sent to, as well as unsubscribe from a certain list.

<img alt="Mailing list in the email footer" />

## Create a list

Go to [Settings -> Lists](https://app.loops.so/settings?page=lists).

<Steps>
  <Step title="Create">
    Click on the **Create a list** button.

    <img alt="Create a new list" />

    A new mailing list will appear. Enter a name for your list and optionally, a description.

    You can also choose a color to easily identify the list inside your Loops account.
  </Step>

  <Step title="Set visibility">
    <img alt="Visibility toggle" />

    Choose between `Private` or `Public` ([see above](#list-types)).
  </Step>

  <Step title="Save">
    Click **Save changes** to finalize the creation of the list.
  </Step>
</Steps>

## Edit a list

To edit an existing list, go to [Settings -> Lists](https://app.loops.so/settings?page=lists).

Edit the name, description, visibility and color.

Click **Save changes** to apply the changes.

<Note>
  After saving your changes, the updated list data will be instantly available
  to your contacts in their Preference Centers.
</Note>

## Delete a list

You can delete lists by clicking on a list's `•••` menu icon and selecting **Delete**.

<img alt="Delete list" />

You cannot delete a list that:

* has been [sent a campaign](#send-campaigns-to-a-list), or has a sending or draft campaign
* is selected in the "Contact added to list" trigger [in a workflow](#trigger-a-workflow-when-a-contact-is-added-to-a-list)
* has been [applied to a workflow](#send-workflows-to-a-list)
* is being used in a [form](/docs/forms/simple-form)

Lists that cannot be deleted will show an "In use" badge. Hover over the badge to see a list of the campaigns, workflows and forms that are stopping the list from being deleted.

## Utilizing lists

Here are a few ways you can use lists to send emails and organize contacts.

### Send campaigns to a list

Instead of sending campaigns to your whole audience, you can send emails to a specific list.

<Steps>
  <Step title="Choose a campaign">
    [Create a campaign](https://app.loops.so/campaigns) or edit an existing one.
  </Step>

  <Step title="Select the list">
    On the [Audience page](https://app.loops.so/audience), select the list you want to send to.

    <img alt="Select a mailing list" />

    Users not subscribed to the selected list will not receive the campaign.
  </Step>

  <Step title="Apply additional filters">
    Optionally, you can apply additional [filters or segments](/docs/contacts/filters-segments) to further refine your audience.
  </Step>
</Steps>

### Send workflows to a list

You can configure workflows to only send to contacts in a specific list (this applies to all triggers).

<Steps>
  <Step title="Choose a workflow">
    [Create a workflow](/docs/workflows) or edit an existing one.
  </Step>

  <Step title="Select the list">
    Select the list you want to trigger the workflow. <img alt="Select the
    list" />
  </Step>

  <Step title="Start the workflow">
    Start the workflow. Only contacts from the selected list will be entered into the workflow.
  </Step>
</Steps>

### Trigger a workflow when a contact is added to a list

This example is a typical use case of sending an email sequence to new contacts when they are added to a specific list.

<Steps>
  <Step title="Choose a workflow">
    [Create a workflow](/docs/workflows) or edit an existing one.
  </Step>

  <Step title="Set the trigger">
    Set the workflow trigger to "Contact added to list".
  </Step>

  <Step title="Select the list">
    Select the list you want to trigger the workflow. <img alt="Select
    trigger" />
  </Step>

  <Step title="Start the workflow">
    Start the workflow. When a contact is added to the selected list, the workflow will be triggered.
  </Step>
</Steps>

### Manually add contacts to lists

How to add existing contacts to your different mailing lists within Loops.

<Warning>
  You cannot add contacts to mailing lists if they are unsubscribed from your audience.\
  Likewise, if a contact unsubscribes from a list via the
  Preference Center, they cannot be resubscribed by your team.
</Warning>

#### Individual contacts

<Steps>
  <Step title="Go to Contacts">
    Go to your [Audience page](https://app.loops.so/audience).
  </Step>

  <Step title="Select a contact">Click on the contact you want to manage.</Step>

  <Step title="Manage subscriptions">
    In the contact details page, click on **Subscribed** to reveal the mailing
    list dropdown. <img alt="Manage
    subscriptions" />
    Toggle each list on or off as needed. Click **Save changes** in the top
    right to apply the changes.
  </Step>
</Steps>

#### Bulk contacts

You can easily add any filtered group of contacts to a specific list on the Audience page.

<Steps>
  <Step title="Go to Audience">
    Go to your [Audience page](https://app.loops.so/audience).
  </Step>

  <Step title="Filter your audience">
    Add filters to segment your audience into the group of contacts you'd like
    to add to a list.
  </Step>

  <Step title="Add contacts to a list">
    Click the `•••` menu icon on the far right-hand side of the audience
    filters, select **Add to mailing list** and then select the list(s) you want
    to add the contacts to. <img alt="Add to
    list" />
  </Step>
</Steps>

### Upload a CSV to a list

If you want to import contacts to a list in bulk you can use our [CSV importer](/docs/add-users/csv-upload).

In the final stage of the form you can select a list, which will add all contacts (new or existing) in the CSV file to that list.

<img alt="CSV upload step selecting a mailing list" />

### Add contacts to lists with the API

Utilizing the [Loops API](/docs/api-reference/intro) you can programmatically add and remove contacts to and from Lists.

<Warning>
  You cannot add contacts to mailing lists if they are unsubscribed from your audience.\
  Likewise, if a contact unsubscribes from a list via the
  Preference Center, they cannot be resubscribed by your team.
</Warning>

When [creating a contact](/docs/api-reference/create-contact), [updating a contact](/docs/api-reference/update-contact), or [sending an event](/docs/api-reference/send-event) with the API, you can include a `mailingLists` object in the payload.

This `mailingLists` object is a key-value pair of list IDs and a subscription status. The subscription status can be `true` or `false`.

```json theme={"dark"}
{
  "email": "test@example.com",
  "mailingLists": {
    "cm06f5v0e45nf0ml5754o9cix": true,
    "cm16k73gq014h0mmj5b6jdi9r": false
  }
}
```

In this example, the contact would be subscribed to `cm06f5v0e45nf0ml5754o9cix` and unsubscribed from `cm16k73gq014h0mmj5b6jdi9r`.

Mailing list IDs can be found [in the app](https://app.loops.so/settings?page=lists) (click the ID to add it to your clipboard) or by using the [API](/docs/api-reference/list-mailing-lists).

<img alt="Visibility toggle" />

### Add contacts to lists with forms

If you use a [form](/docs/forms/simple-form) on your website you can subscribe contacts to specific lists.

When exporting HTML from the [Forms page](https://app.loops.so/forms) in Loops, choose a list from the **Settings** tab.

<img alt="Select a list from the form settings" />

<Note>
  Adding contacts to a list via a form only works with public lists. The option
  to select a list will only appear in the form settings if you have at least
  one public list.
</Note>

If you already have a form in place or are using a [custom form](/docs/forms/custom-form) you can add a `mailingLists` parameter to the form body with the value a comma-separated list of mailing list IDs.

<CodeGroup>
  ```html HTML example {3} theme={"dark"}
  <form>
    <input type="email" name="email" required />
    <input type="hidden" name="mailingLists" value="cm06f5v0e45nf0ml5754o9cix,cm16k73gq014h0mmj5b6jdi9r" />
  </form>
  ```

  ```javascript JavaScript example {4} theme={"dark"}
  fetch(formEndpointUrl, {
    method: "POST",
    body:
      "mailingLists=cm06f5v0e45nf0ml5754o9cix,cm16k73gq014h0mmj5b6jdi9r" +
      "&email=" +
      encodeURIComponent(emailInput.value),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });
  ```
</CodeGroup>


# Contact properties
Source: https://loops.so/docs/contacts/properties

How to add, edit and delete contact properties.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

Contact properties are fields you store on each contact in Loops (like name, plan, or signup source). You can use them to personalize emails, segment your audience, and trigger workflows.

Loops includes a set of default contact properties and lets you create custom properties for your product.

## Editing contacts

You can edit a contact's properties in the [Audience](https://app.loops.so/audience) page by clicking on a contact. Edit the contact's information in the form and click **Save changes**. If you want your changes to the contact to trigger any related workflows, select **Save and trigger** instead.

You can also edit a contact's properties via [the API](/docs/api-reference/update-contact) or one of our [integrations](/docs/integrations).

[CSV uploads](/docs/add-users/csv-upload#update-contacts-via-csv) are a good way to edit multiple contacts in bulk.

## Default contact properties

These are the default properties for every contact on Loops. They cannot be deleted.

| Contact Property | Example                                       | Email Tag\*   | API Name\*\* |
| ---------------- | --------------------------------------------- | ------------- | ------------ |
| Email            | *[test@example.com](mailto:test@example.com)* | `{email}`     | `email`      |
| First Name       | *Chris*                                       | `{firstName}` | `firstName`  |
| Last Name        | *Frantz*                                      | `{lastName}`  | `lastName`   |
| Notes            | *Favorite color is blue.*                     | `{notes}`     | N/A          |
| Source           | *API*                                         | `{source}`    | `source`     |
| Subscribed       | `true`                                        | N/A           | `subscribed` |
| User Group       | *Investors*                                   | `{userGroup}` | `userGroup`  |
| User Id          | *ask523236*                                   | `{userId}`    | `userId`     |

\* Used to [add personalization to emails](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax).\
\*\* Used in [API requests](/docs/api-reference/intro).

### Source

"Source" describes where the contact originated from.

By default, this value will be "Form" for contacts added [via a form](/docs/forms/simple-form), or "API" for contacts added [via the API](/docs/api-reference/intro). You can specify custom "Source" values when adding contacts via forms and the API.

### Subscribed

The "Subscribed" value determines whether a contact is able to receive **workflow emails and campaigns**. Unsubscribed contacts *will continue to receive all transactional emails*.

Contacts can unsubscribe from your emails using an [Unsubscribe link](/docs/creating-emails/editor#footer-content) automatically added to your campaigns and workflows.

#### Using `subscribed` in API requests

When you include a `subscribed` value in your API requests, you will manually change the contact's subscribed status. For example, if you send `subscribed: true` when updating a contact, previously unsubscribed contacts will be re-subscribed.

<Tip>
  We recommend leaving `subscribed` out of your API requests unless you
  specifically want to unsubscribe or re-subscribe a contact. All new contacts
  are subscribed by default (`subscribed: true`).
</Tip>

### Some important notes:

* We do not charge for unsubscribed contacts.
* We suggest you keep unsubscribed contacts in your audience. If you delete and then re-add them in the future somehow, they may end up being "subscribed" even though they have been unsubscribed.
* You can re-subscribe unsubscribed contacts [with the API](/docs/api-reference/update-contact) and with some of [our integrations](/docs/integrations#manage-contacts). You cannot re-subscribe contacts via a [CSV upload](/docs/add-users/csv-upload) or from the Audience page in Loops. Unsubscribes from [mailing lists](/docs/contacts/mailing-lists) work differently. You cannot re-subscribe contacts to lists via any method.

### User Group

"User Group" is a useful optional property that you can use to segment contacts. It is a free text field that allows you to easily divide contacts into groups like "Users", "VIPs", "Investors" or "Customers".

Contacts can currently only have one user group value.

### User Id

"User Id" is a unique external ID you can assign to each contact in your audience. For example, this could be a customer ID from your store or a user ID from your SaaS.

This field is optional but is very useful if you are working with our API. For example, you need a user ID to be able to [change a contact's email address](/docs/api-reference/update-contact).

### Mailing lists

Read about [how to use mailing lists](/docs/contacts/mailing-lists) in Loops.

## Custom contact properties

Custom contact properties are additional fields that you can create to store information about contacts.

### Types of property

Custom contact properties can be one of four different types:

* String
* Number
* Boolean
* Date (see below)

You can specify a property type when creating new properties in Loops.

#### Dates

When sending dates with the [API](/docs/api-reference/intro) or via one of our [integrations](/docs/integrations), you can use either a Unix timestamp (*in milliseconds*) or an [ECMA-262 date-time string](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format).

Timestamps must be in milliseconds and can be sent as either an integer or string.

* `1705486871000` (if the Unix timestamp is `1705486871`)

Supported date formats are shown below. These must be sent as a string.

Adding a time offset at the end (e.g. `+02:00` or `-07:00`) is optional (if omitted, the date will default to UTC).

* `YYYY-MM-DDTHH:mm:ss.sss`
* `YYYY-MM-DDTHH:mm:ss`
* `YYYY-MM-DDTHH:mm`
* `YYYY-MM-DD HH:mm:ss.sss`
* `YYYY-MM-DD HH:mm:ss`
* `YYYY-MM-DD HH:mm`
* `YYYY-MM-DD`

<Info>
  [CSV uploads](/docs/add-users/csv-upload) accept dates in the formats listed above, but do not support timestamps.
</Info>

### Reserved names

Note that Loops does not allow the creation of properties with the following reserved names:

* `id`
* `listId`
* `softDeleteAt`
* `teamId`
* `updatedAt`

### Add a property

One way to create custom contact properties is to go to [your Audience](https://app.loops.so/audience) and click on any of the column headers, then select **Add property**.

<img alt="Audience column menu showing “Add property”" />

Alternatively you can scroll to the end of the Audience table and click the `+` button at the end of the column headers.

<img alt="Plus button at end of Audience columns to add a property" />

A third way is to go to your [API Settings](https://app.loops.so/settings?page=api) page, scroll down to the Contact properties section and click **Add property**.

In each of these cases, you'll see a form asking you for a property name and type.

<img alt="Add property form with name and type fields" />

It's also possible to [create contact properties with the API](/docs/api-reference/create-contact-property).

```json theme={"dark"}
POST https://app.loops.so/api/v1/contacts/properties

{
  "name": "myCustomProperty",
  "type": "string"
}
```

### Editing contact properties

You cannot edit a contact property after it has been created. If you need to change the name or type of a property, you will need to create a new one.

### Deleting contact properties

You can delete properties on the Audience page by clicking on column headers.

<img alt="Audience column menu showing “Delete property”" />

You can also delete properties from your [API Settings](https://app.loops.so/settings?page=api) page by clicking the trashcan icons.

It is not possible to delete default contact properties.

<Warning>
  Once a contact property is deleted, all associated data will also be deleted
  and cannot be recovered. It's important to be sure that you won't need the
  information stored in a property before deleting it.
</Warning>

#### Property in use warning

If you receive a “Property in use” warning modal while deleting a contact property, there are a few things you can check before you're able to delete the property.

* If the listed email is a Campaign:
  * Check if the property is in use as [dynamic content](/docs/creating-emails/personalizing-emails) inside the email editor
  * Ensure this property is not being actively used in the Audience filter
* If the listed email is inside a workflow:
  * Make sure a draft or running workflow is not using it as part of the Audience filter or as a Trigger

<img alt="Delete property confirmation dialog" />


# Contact suppression
Source: https://loops.so/docs/contacts/suppression

Check suppression status and remove suppressions with the API.

When an email to a contact results in a hard bounce or a complaint, Loops suppresses sending email to the contact to protect your sending reputation. Attempted sends to this contact will appear as hard bounces.

Suppressed contacts won't receive further emails sent via Loops. In order for a contact to receive emails again, you need to remove the suppression.

<Info>
  You should typically only remove suppression if you're confident the address is now deliverable (for example, the mailbox was restored).
</Info>

## Check if a contact is suppressed

You can use the [Check suppression](/docs/api-reference/check-contact-suppression) endpoint to check if a contact is suppressed and to view your current suppression removal quota.

```
GET https://app.loops.so/api/v1/contacts/suppression?email=test@example.com
```

## Remove suppression for a contact

<Warning>
  Free Loops accounts can only request suppression removal via our [support channels](https://app.loops.so/settings?page=support).
</Warning>

Paid accounts can use the [Remove suppression](/docs/api-reference/remove-contact-suppression) endpoint to remove a suppression.

```
DELETE https://app.loops.so/api/v1/contacts/suppression?email=test@example.com
```

## Suppression removal quota

Paid accounts can request up to 100 suppression removals in a rolling 30 day period.

You can view your current suppression removal quota by using the [Check suppression](/docs/api-reference/check-contact-suppression) endpoint.


# Components
Source: https://loops.so/docs/creating-emails/components

Create reusable Loops components for your emails.

Components are reusable elements for emails. They help you create frequently-repeated sections of emails once, which you can then easily drop into new emails.

<img alt="Components in an email" />

Changes made to components can be synced to all other instances. You can also choose to make local edits to a single component instance without updating others.

Components can be created from and added to all emails created in Loops (campaign, workflow and transactional emails). They also work in both Plain and Styled emails.

## Example components

Some useful examples of components are logos and social icons. These elements are typically the same across multiple emails; using components will make sure they are the same everywhere.

Most of the time you will want your logo to have the same alignment, spacing and size in your emails. Similarly, you will want the same set of social icons readily available to drop into every new email you create.

<img alt="Example components" />

## Create a component

To create a component, click on an element in your email and then the **Create component** icon in the editor panel.

<Tip>
  You can also create and manage components [via the API](/docs/api-reference/create-component).
</Tip>

<img alt="Create a component" />

A modal will appear where you can name your component. Use a descriptive name so you can easily find your component in the future.

Click **Create**. You will see your new component appear in the Components list on the left of the editor.

<Note>
  All components you create are available in all of your emails, i.e. a
  component created in a campaign email is also available to insert into
  transactional emails.
</Note>

## Insert a component

Click on **Components** at the top of the left-hand panel to reveal your components list, then simply click on a component to insert it.

<img alt="Add a component" />

If you want to change the location of a component in your email, you can drag and drop it within the editor just like other blocks, by using the six dot menu icon.

If you already have a component in your email, clicking **Duplicate** in the **Block styles** editor panel will add a copy of that component into the email, including any local edits made.

## Edit a component

<Tip>
  You can tell if an element in your email is a component by looking for a
  purple outline around the block in the editor.
</Tip>

To edit a component, edit its content as you would any other part of your email.

This will create local edits to that single instance; these edits are not synced to other components in use elsewhere. This means you can make tweaks and changes to a single instance of a component without updating all other instances.

If a component has local edits, you will notice the **Push changes** and **Reset changes** icons are now active.

<img alt="Edited component" />

If you want to save your changes to all instances of the component, click on the **Push changes to main component** button. Note that any local edits made to the component in other emails will be preserved.

<img alt="Push changes to other instances" />

If you make local edits to a component that you want to revert, click the **Reset component changes** option.

<img alt="Reset component changes" />

## Rename a component

In the Components list, find the component you want to rename, then click the `•••` menu icon. Click on **Rename** to show the rename modal.

Enter a new name and click **Rename**.

## Delete a component

<Warning>
  Deleting a component only deletes the component; it does not remove the
  component from emails. A deleted component's contents will be retained by any
  email it was added to.
</Warning>

In the Components list, find the component you want to delete, then click the `•••` menu icon. Click on **Delete** to show the confirmation modal.

Click **Delete** to delete the component.


# Duplicating emails
Source: https://loops.so/docs/creating-emails/duplicating-emails

Reuse basic elements of an email or create email templates.

Duplicating emails is a great way to save time crafting similar emails.

You can easily create emails that share many of the same elements (investor updates, product updates, waitlist invites, etc).

## Campaigns and transactional emails

To duplicate an email:

1. Go into **Campaigns** or **Transactional**.
2. Click the `•••` menu icon and select **Duplicate**.
3. Edit the new email's subject line, preview text and content as needed.

<img alt="Duplicating emails" />

## Workflows

With workflows, you can duplicate a full workflow (which may contain many emails) using the method above (click a `•••` menu icon within the **Workflows** list page).

However, you may also duplicate emails within a workflow by clicking the **Copy existing email** button in the inspector panel on the right-hand side after creating a new email node. This will let you copy and insert any email from any of your existing workflows, including the currently opened workflow.

<img alt="Duplicating emails" />


# The editor
Source: https://loops.so/docs/creating-emails/editor

Loops' email editor has writing mode, themes, dynamic personalization, reusable content blocks, keyboard/Markdown shortcuts and more.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

We have built a powerful editor for creating emails. Create any email with our flexible design tools, and write distraction-free with Writing mode.

Here are a few of the editor's stand-out features:

* Customizable custom themes to keep your designs consistent
* Writing mode, distraction-free editing which hides the editor interface
* Auto-saves as you type
* Mobile-friendly so you can write and design emails on any device
* Support for images, buttons, social icons, dividers and columns
* The same interface for marketing and transactional emails
* Personalize emails with contact and event properties
* Helpful keyboard and Markdown shortcuts for adding content and formatting text

<Tip>
  If you would rather upload emails into Loops, you can [import
  MJML](/docs/creating-emails/uploading-custom-email) instead of using the editor.
</Tip>

## Formatting text

Once you have written content in your email, you will see formatting options appear in the right-hand panel. These options will change depending on the element you have selected in the email.

<img alt="Email editor formatting toolbar options" />

You can choose between different headings, style text **bold**, *italic*, <u>underline</u> as well as add links. You can also change text size and color.

Text can also be easily changed into lists, quotes and [code blocks](/docs/creating-emails/editor/code-blocks).

<img alt="Formatted text options in the Loops email editor" />

<Note>
  You can also use [keyboard shortcuts](#keyboard-shortcuts) and [Markdown formatting](/docs/creating-emails/editor/markdown) to format your text.
</Note>

## Writing mode

To focus on writing your email, you can enter Writing mode.

To toggle writing mode, use the switch just above the editor or use the `⌘+/` (Mac) or `Ctrl+/` (Windows) keyboard shortcut.

<img alt="Writing mode" />

When in Writing mode, you can edit styles by clicking the **Show styles** button that appears above selected elements. This will open a slide-in editor panel on the right.

<img alt="Writing mode styles" />

## Adding elements

Emails can be more than just text. You can add the following elements to your emails:

* images
* links
* buttons
* dividers
* social icons
* columns
* sections
* arrays

You can add elements from the toolbar above the editor, or by hovering over an existing element in your email and clicking the `+` plus icon on the left.

<img alt="Adding an element using the plus icon in the editor" />

You can also add elements by typing `/` in the editor:

<img alt="Slash menu for inserting email blocks" />

If you continue typing, you can narrow down the selections. You can use your arrow keys to select items and pressing Enter will confirm your selection.

<img alt="Filtering slash menu options by typing" />

When adding dynamic content like contact properties to [personalize emails](/docs/creating-emails/personalizing-emails), you can type "\{" to open the dynamic content menu.

<img alt="Dynamic content menu opened by typing the opening brace" />

<Note>
  You can also use [keyboard shortcuts](#keyboard-shortcuts) and [Markdown formatting](#markdown-formatting) to add certain elements to your email.
</Note>

<CardGroup>
  <Card title="Images" icon="image" href="/creating-emails/editor/images" />

  <Card title="Videos" icon="video" href="/creating-emails/editor/videos" />

  <Card title="Links" icon="link" href="/creating-emails/editor/links" />

  <Card title="Buttons" icon="rectangle-minus" href="/creating-emails/editor/buttons" />

  <Card title="Social icons" icon="star" href="/creating-emails/editor/social-icons" />

  <Card title="Dividers" icon="minus" href="/creating-emails/editor/dividers" />

  <Card title="Columns" icon="columns" href="/creating-emails/editor/columns" />

  <Card title="Sections" icon="rectangle-list" href="/creating-emails/editor/sections" />

  <Card title="Arrays" icon="bars" href="/creating-emails/editor/arrays" />

  <Card title="Markdown" icon="markdown" href="/creating-emails/editor/markdown" />

  <Card title="Code blocks" icon="code" href="/creating-emails/editor/code-blocks" />
</CardGroup>

## Reordering content

If you hover over a block in your email, you'll see a drag-and-drop icon appear on the left. You can drag this icon to re-order elements in your email.

<img alt="Drag handles for reordering blocks in the editor" />

## Footer content

We add an automatic footer into campaign and workflow emails, which includes your company name, address and an unsubscribe link.

<img alt="Default email footer with unsubscribe link" />

Unsubscribe links help prevent your messages from being marked as spam and ensure that your messages are willingly being received by an engaged audience, helping your deliverability.

The unsubscribe link leads to a [Preference Center](/docs/contacts/mailing-lists#preference-center), where your contacts can manage their own subscriptions.

You can edit your company address from [Settings -> Domain](https://app.loops.so/settings?page=domain). The address shown there is what appears in every email footer and can't be fully removed. A physical address and unsubscribe link are required for deliverability and anti-spam compliance.

If your email was sent to a [mailing list](/docs/contacts/mailing-lists), the footer will also [include the name of the list](/docs/contacts/mailing-lists#email-footers) the email was sent to.

### Editing footer content

The automatic footer can't be edited directly, but we have a manual system for creating custom footers on paid plans. [Contact support](mailto:help@loops.so) for more information.

For [custom MJML emails](/docs/creating-emails/uploading-custom-email), design your footer however you like and include the `{unsubscribe_link}` tag on your unsubscribe link.

## Keyboard shortcuts

The following keyboard shortcuts are available in the editor.

<Tip>
  You can also use [Markdown formatting](/docs/creating-emails/editor/markdown) to format your text.
</Tip>

<Info>
  The editor automatically saves your work as you type so there is no need to
  save manually.
</Info>

<Tabs>
  <Tab title="Mac">
    | Shortcut      | Action                           |
    | :------------ | :------------------------------- |
    | `⌘` `/`       | Toggle writing mode              |
    | `/`           | Insert a block (open slash menu) |
    | `⌘` `D`       | Duplicate a block                |
    | `⌘` `K`       | Insert a link                    |
    | `⌘` `B`       | Bold text                        |
    | `⌘` `I`       | Italicize text                   |
    | `⌘` `U`       | Underline text                   |
    | `Tab`         | Indent a list item               |
    | `Shift` `Tab` | Unindent a list item             |

    You can also use the following default keyboard shortcuts:

    | Shortcut        | Action     |
    | :-------------- | :--------- |
    | `⌘` `A`         | Select all |
    | `⌘` `C`         | Copy       |
    | `⌘` `V`         | Paste      |
    | `⌘` `X`         | Cut        |
    | `⌘` `Z`         | Undo       |
    | `⌘` `Shift` `Z` | Redo       |
  </Tab>

  <Tab title="Windows">
    | Shortcut      | Action                           |
    | :------------ | :------------------------------- |
    | `Ctrl` `/`    | Toggle writing mode              |
    | `/`           | Insert a block (open slash menu) |
    | `Ctrl` `D`    | Duplicate a block                |
    | `Ctrl` `K`    | Insert a link                    |
    | `Ctrl` `B`    | Bold text                        |
    | `Ctrl` `I`    | Italicize text                   |
    | `Ctrl` `U`    | Underline text                   |
    | `Tab`         | Indent a list item               |
    | `Shift` `Tab` | Unindent a list item             |

    You can also use the following default keyboard shortcuts:

    | Shortcut           | Action     |
    | :----------------- | :--------- |
    | `Ctrl` `A`         | Select all |
    | `Ctrl` `C`         | Copy       |
    | `Ctrl` `V`         | Paste      |
    | `Ctrl` `X`         | Cut        |
    | `Ctrl` `Z`         | Undo       |
    | `Ctrl` `Shift` `Z` | Redo       |
  </Tab>
</Tabs>


# Arrays
Source: https://loops.so/docs/creating-emails/editor/arrays

Arrays let you add repeatable content blocks to transactional emails, populated at send time via data variables.

Arrays are a way to add repeatable sections to an email allowing you to add a dynamic list of items, like products.

You can display up to 100 items in each array block.

<Info>
  Arrays are currently supported in transactional emails.
</Info>

## Adding arrays

You can add array blocks to your email by using the button in the editor toolbar or typing `/` and selecting **Array**.

<img alt="Add an array" />

Arrays are assigned a data variable name (default is `items`) which can be changed in the editor panel in the **Array** section.

<img alt="Editor panel with array section" />

## Adding content to arrays

Once the array block is in your email, you can add content inside, which will be repeated for each item in the array.

For example, if you have an array of products and you want to show a name and a description for each product, you can add a heading and a text block inside the array block.

<img alt="Array block with example product inside" />

## Adding dynamic content

You can add dynamic content to array blocks by inserting [data variables](/docs/transactional#add-data-variables). These data variables can then be populated with data when you [send your email](/docs/transactional#send-your-email).

You can add data variables by using the button above the editor, typing `{`, or typing `/` and selecting **Data variable**.

<img alt="Add a data variable" />

By default, data variables you add will be available within the array block only. You can change variable tags inside array blocks to be "global" by changing the **Array item** option in the right sidebar to **No**.

<img alt="Global toggle" />

## Previewing arrays

You can preview how multiple items will look in your email by using the `+` and `-` buttons in the **Preview** section of the editor sidebar. These buttons allow you to see how the array will look in your email with different numbers of items.

<img alt="Preview items" />


# Buttons
Source: https://loops.so/docs/creating-emails/editor/buttons

Add styled, clickable buttons to your Loops emails.

You can add buttons to your email to allow recipients to take action.

Buttons are styled using the **Block styles** editor panel and can contain text.

You can add links to buttons by clicking the `+` icon in the **Link** editor panel.

Add buttons:

* by using the button icon in the editor toolbar
* by typing `/` in the editor and selecting **Button**
* by hovering over an existing element in your email and clicking the `+` plus icon on the left and selecting **Button**

## Dynamic content

To add [dynamic content](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax) to button text, you need to manually type the correct tag syntax (e.g. `{firstName}`, `{EVENT_PROPERTY:firstName}`, `{DATA_VARIABLE:firstName}`) directly into the button field.

Adding dynamic content to buttons through the UI is not yet supported.

<CardGroup>
  <Card title="Dynamic content" href="/creating-emails/personalizing-emails#dynamic-tag-syntax" icon="crystal-ball">
    Read about adding dynamic content in emails using the tag syntax.
  </Card>
</CardGroup>


# Code blocks
Source: https://loops.so/docs/creating-emails/editor/code-blocks

Add code blocks to your email

## Code blocks

Code blocks are a way to display code in your email.

<img alt="Code block example" />

They are displayed in a monospace font with a gray background. You can edit the background color in the **Block styles** editor panel, along with padding and corner border radius.

Note that no syntax highlighting is added to code blocks because of email client compatibility.

You can add code blocks to your email by typing three backticks ` ``` `.

Alternatively, you can use the `/` slash menu or the `+` plus menu on the left of the editor, and then select **Code Block**.

You can also select **Code Block** from the **Text** panel in the right editor sidebar.

## Inline code

Add inline code to your email (like `this inline code`) by typing a single backtick `` ` ``.


# Columns
Source: https://loops.so/docs/creating-emails/editor/columns

Arrange email content side-by-side with layouts of 2 to 4 columns.

Columns allow you to arrange content side by side, creating dynamic layouts for your emails. You can use columns to place elements like images, text, buttons, and more next to each other.

<Note>
  You can use layouts of 2 to 4 columns. Nesting columns within columns is not
  yet supported.
</Note>

## Adding columns

You can add columns to your email in several ways:

* **Using the editor toolbar**: Click on the columns icon to insert a column layout.
* **Using the `/` slash menu in the editor**: Type `/` to open the slash menu then start typing "columns" or click on the **Columns** option.
* **Using the `+` icon**: hover over an existing element in your email and click the `+` plus icon on the left, then select **Columns**.
* **Dragging two blocks together**: Click and hold the block settings icon (six dots) to the left of a block. Drag the block next to another one until you see a vertical gray line on the right, which indicates a new column will be created.
  <img alt="Adding columns by dragging blocks together" />

## Customizing columns

Once you've added columns, you can customize their appearance and behavior to suit your design needs.

### Number of columns

You can choose the number of columns you want to use by clicking on the number of columns you want to use in the **Block styles** editor panel.

<img alt="Number of columns option" />

If you decrease the number of columns, the content of the farthest right column is moved to the bottom of the new right-most column.

### Width and spacing

You can adjust the width of a column by clicking and dragging the gray bar between the columns.

<img alt="Adjusting column width" />

You can also adjust how much space appears between columns by dragging the green bar between the columns.

<img alt="Adjusting column spacing" />

### Stacking columns on mobile

When emails are viewed on a mobile device (when there is less horizontal space available) you can choose if your columns should stack vertically for better readability or stay as columns.

In the **Block styles** editor panel you'll find the option to stack columns vertically on mobile (when the viewport is 479px or smaller).

<img alt="Mobile stacking option" />

### Adding and moving content

You can drag elements like text blocks, images, and buttons in and out of your columns.

To do this, hold down the `⌘` key (Mac) or `Ctrl` key (Windows) while hovering over the element you want to move. Then click and drag the block settings icon (six dots) to move the block to its new position.

<img alt="Drag an element into a column" />

## Tips

* **No nested columns**: Keep in mind that nesting columns within columns is not supported. If you need a complex layout, consider adjusting your design to fit within the 2 to 4 column limit.
* **Consistent styling**: Use the block settings to ensure that padding and alignment are consistent across your columns for a polished look.


# Dividers
Source: https://loops.so/docs/creating-emails/editor/dividers

Separate or group content in your Loops emails with dividers.

You can add dividers to your email to separate or group content.

Add dividers:

* by using the divider icon in the editor toolbar
* by typing `/` in the editor and selecting **Divider**
* by hovering over an existing element in your email and clicking the `+` plus icon on the left and selecting **Divider**
* by typing `---` and hitting the space bar


# Images
Source: https://loops.so/docs/creating-emails/editor/images

Insert and link images in Loops emails, including dynamic images sourced from contact properties, event properties, or data variables.

Add images:

* by using the image button in the editor toolbar
* by dragging or pasting images into the editor
* by typing `/` in the editor and selecting **Image**
* by hovering over an existing element in your email and clicking the `+` plus icon on the left and selecting **Image**

Images must be under 4 MB in size.

Add a link to an image by selecting the element and then clicking the `+` button in the **Link** editor panel on the right.

## Dynamic images

You can also insert images hosted outside of Loops into your emails.

First, add a placeholder image to your email (which you can use to specify the location and size of the image) and then click `+` in the **Dynamic source** section in the right-hand panel.

<Warning>
  Keep in mind that any images you insert must be publicly accessible and the
  URL must end with a supported image file extension for email (like `.jpg` or
  `.png`). Additionally, these images should be hosted for a period of time that
  is relevant to the email's lifecycle as they will not be stored in Loops.
</Warning>

Paste in an image URL or create dynamic URLs by clicking the icon on the right.

For example, you could insert an `imageUrl` data variable in a transactional email, or use an `avatarUrl` custom contact property in a campaign.

<img alt="Adding a dynamic image" />

You can also create dynamic URLs by combining text and dynamic content inside the image source field.

<img alt="Adding a dynamic image URL" />

<Note>
  The placeholder image in the editor will not update when you add a dynamic
  source. In order to test dynamic images, you can send [preview
  emails](/docs/sending-first-email#preview-your-email).
</Note>

<Tip>
  Note that, just as with other dynamic content, if the contact or event
  property used for your dynamic image is missing, the email will not be sent.
</Tip>


# Links
Source: https://loops.so/docs/creating-emails/editor/links

Add static and dynamic links to text, images, and buttons in Loops emails, append UTM parameters, and control per-link click tracking.

You can add links to text, images and buttons. First, select the text or element and click the `+` icon in the **Link** editor panel. You can also use the `⌘+K` keyboard shortcut.

<img alt="Add a link" />

Enter your URL into the field then hit Enter. Helpfully, you can omit the `https://` protocol as it is automatically added to each link.

## Dynamic links

You can create dynamic links using contact and event properties or data variables.

When editing a link, click the icon—which will be different depending on the type of email you are creating—on the right of the input, then add the dynamic content you want to use.

<img alt="Dynamic links" />

## UTM parameters

Loops does not automatically add UTM parameters to links in your emails. To attribute traffic and conversions in tools like Google Analytics, append UTM parameters directly to your link URLs when you add them:

```
https://example.com/pricing?utm_source=loops&utm_medium=email&utm_campaign=launch
```

You can combine UTM parameters with [dynamic links](#dynamic-links) to vary values per contact or per send—for example, using a data variable for `utm_campaign`.

## Link tracking

By default marketing emails (campaigns and workflows) have opens and link tracking enabled. This can be disabled for all emails in [Settings -> Sending](https://app.loops.so/settings?page=sending).

<Note>
  Links and opens in transactional emails are never tracked in order to improve
  deliverability.
</Note>

<img alt="Click and open tracking setting" />

If you want to disable click tracking on individual links in your emails, click the icon in the editor panel.

<img alt="Disable tracking" />

<Warning>
  We do not support deeplinks prepended with `ms-word:` as they are blocked by Gmail and other inbox providers. We recommend linking to a non-deeplink landing page instead. This may also be the case with other deeplinks in certain situations—you'll know if that's the case as the link will seem unclickable.
</Warning>


# Markdown
Source: https://loops.so/docs/creating-emails/editor/markdown

Write and format Loops email content with Markdown shortcuts for headings, lists, quotes, bold, and links.

The editor has full support for Markdown, allowing you to write and format content quickly using familiar syntax.

## Markdown formatting

You can use Markdown to add content to your emails. For example, you can add a heading by typing `#` followed by a space.

It's also possible to format text with Markdown, like adding bold text using `**`, or inserting links using `[text](url)`.

Add different content blocks using the Markdown formatting below followed by a space.

| Syntax         | Action              |
| :------------- | :------------------ |
| `#`            | Add a heading 1     |
| `##`           | Add a heading 2     |
| `###`          | Add a heading 3     |
| `>`            | Add a quote         |
| `1.`           | Add a numbered list |
| `-` or `*`     | Add a bulleted list |
| ` ```code``` ` | Add a code block    |

You can format text with the following shortcuts:

| Syntax                      | Action              |
| :-------------------------- | :------------------ |
| `**text**` or `__text__`    | Bold text           |
| `*text*` or `_text_`        | Italicize text      |
| `~~text~~`                  | Strike through text |
| `[Loops](https://loops.so)` | Add a link          |
| `` `code` ``                | Inline code         |

## Pasting markdown

The editor supports pasting Markdown content, making it seamless to bring content from other platforms. Paste Markdown with `⌘+Shift+V` (Mac) or `Ctrl+Shift+V` (Windows) and it will be converted into native editor blocks.

## Custom markdown tags

When pasting markdown, you can use custom Loops tags to add buttons and columns directly in your content:

**Columns**

```html theme={"dark"}
<loops-columns>
  <loops-column-item>
    ### Column 1 heading
    Column *1* text
  </loops-column-item>
  <loops-column-item>
    ### Column 2 heading
    Column *2* text
  </loops-column-item>
</loops-columns>
```

**Buttons**

```html theme={"dark"}
<loops-button href="https://loops.so">Subscribe</loops-button>
```

These custom tags can be mixed with regular markdown formatting.


# Sections
Source: https://loops.so/docs/creating-emails/editor/sections

Group blocks into reusable sections to structure your email layout.

Sections allow you to group blocks together and add styling options to the whole section at once. This makes card-style layouts much easier to build.

<img alt="A section in an email" />

## Adding sections

Add sections:

* by using the section button in the editor toolbar
* by typing `/` in the editor and selecting **Section**
* by hovering over an existing element in your email and clicking the `+` plus icon on the left and selecting **Section**

<Note>
  Classic Outlook may render extra padding around sections due to its legacy rendering engine.
</Note>

## Conditional sections

Sections can be shown or hidden for each recipient based on a [contact property](/docs/contacts/properties), [event property](/docs/events/properties), or [data variable](/docs/transactional#add-data-variables) value. This is useful for content that only applies to some people, like a plan-specific callout or a promo banner.

First, add a section to your email, then open the **Condition** panel in the editor sidebar.

<img alt="The Condition panel in the editor sidebar" />

Set **Show when** to build a condition from up to three parts:

1. **Variable** — the field to check:
   * In campaigns and workflows, choose a [contact property](/docs/contacts/properties).
   * In workflows triggered by events, you can also choose an [event property](/docs/events/properties).
   * In [transactional emails](/docs/transactional), type the name of a [data variable](/docs/transactional#add-data-variables).
2. **Operator** — how to compare it (see below).
3. **Value** — what to compare against. Only shown for operators that need one, such as **Equals** or **Contains**.

For example, `email` / **Does not contain** / `loops.so` shows the section only for recipients whose email address does not contain `loops.so`.

Leave **Show when** blank to show the section to every recipient.

In transactional emails, you can create a new variable just for conditional sections, like `showPromo`. The data variable used does not need to be in the email content itself. Any condition-only data variables are accepted in the [Send transactional API call](/docs/api-reference/send-transactional-email) like regular data variables.

### Operators

The available operators depend on the variable's type. Data variables are always treated as text.

| Type    | Operators                                                                  |
| ------- | -------------------------------------------------------------------------- |
| Text    | Is not empty, Is empty, Equals, Does not equal, Contains, Does not contain |
| Number  | Is not empty, Is empty, Equals, Does not equal, Greater than, Less than    |
| Boolean | Is not empty, Is empty, Is true, Is false                                  |

A missing or empty value never fails the send; it just hides the section, so condition variables are optional at send time.

<Note>
  Everything inside a hidden section is removed from the email, including any variables, arrays or links it contains.
</Note>

Conditions can also be authored in [LMX](/docs/creating-emails/lmx#conditional-sections) with the `if`, `ifOperation`, and `ifValue` attributes.

## Clickable sections

Sections become clickable when you add a link in the **Link** editor panel, which is useful for:

* product cards
* account-summary callouts
* any layout where a section should link somewhere


# Social icons
Source: https://loops.so/docs/creating-emails/editor/social-icons

Add a row of social platform icons to your Loops email footer.

You can add a row of social icons to your emails. There are hundreds to choose from.

<img alt="Social icons" />

## Adding icons

Add a social icon block:

* by selecting the globe icon in the editor toolbar
* by typing `/` in the editor and selecting **Icons**
* by hovering over an existing element in your email and clicking the `+` plus icon on the left and selecting **Icons**

You have options for icon size, gap and color (black, gray or white). You can add more icons by clicking the `+` icon in the editor panel.

To rearrange icons, you can drag them around in the editor or in the Links list in the editor panel.

You can also change padding, alignment and more using the **Block styles**.

## Available icons

| Company                         | Icon name                        |
| ------------------------------- | -------------------------------- |
| 42 Group                        | `42-group`                       |
| 500px                           | `500px`                          |
| Accessible Icon                 | `accessible-icon`                |
| Accusoft                        | `accusoft`                       |
| Adn                             | `adn`                            |
| Adversal                        | `adversal`                       |
| Affiliatetheme                  | `affiliatetheme`                 |
| Airbnb                          | `airbnb`                         |
| Algolia                         | `algolia`                        |
| Alipay                          | `alipay`                         |
| Amazon                          | `amazon`                         |
| Amazon Pay                      | `amazon-pay`                     |
| Amilia                          | `amilia`                         |
| Android                         | `android`                        |
| AngelList                       | `angellist`                      |
| Angrycreative                   | `angrycreative`                  |
| Angular                         | `angular`                        |
| App Store                       | `app-store`                      |
| App Store iOS                   | `app-store-ios`                  |
| Apper                           | `apper`                          |
| Apple                           | `apple`                          |
| Apple Pay                       | `apple-pay`                      |
| Apple Podcast                   | `apple-podcast`                  |
| Arch Linux                      | `arch-linux`                     |
| Artstation                      | `artstation`                     |
| Asymmetrik                      | `asymmetrik`                     |
| Atlassian                       | `atlassian`                      |
| Audible                         | `audible`                        |
| Autoprefixer                    | `autoprefixer`                   |
| Avianex                         | `avianex`                        |
| Aviato                          | `aviato`                         |
| AWS                             | `aws`                            |
| Bandcamp                        | `bandcamp`                       |
| Battle Net                      | `battle-net`                     |
| Behance                         | `behance`                        |
| Behance (square)                | `square-behance`                 |
| Bilibili                        | `bilibili`                       |
| Bimobject                       | `bimobject`                      |
| Bitbucket                       | `bitbucket`                      |
| Bitcoin                         | `bitcoin`                        |
| Bity                            | `bity`                           |
| Black Tie                       | `black-tie`                      |
| Blackberry                      | `blackberry`                     |
| Blogger                         | `blogger`                        |
| Blogger B                       | `blogger-b`                      |
| Bluesky                         | `bluesky`                        |
| Bluesky (square)                | `square-bluesky`                 |
| Bluetooth                       | `bluetooth`                      |
| Bluetooth B                     | `bluetooth-b`                    |
| Board Game Geek                 | `board-game-geek`                |
| Bootstrap                       | `bootstrap`                      |
| Bots                            | `bots`                           |
| Brave                           | `brave`                          |
| Brave Reverse                   | `brave-reverse`                  |
| Btc                             | `btc`                            |
| Buffer                          | `buffer`                         |
| Buromobelexperte                | `buromobelexperte`               |
| Buy N Large                     | `buy-n-large`                    |
| BuySellAds                      | `buysellads`                     |
| Canadian Maple Leaf             | `canadian-maple-leaf`            |
| Cash App                        | `cash-app`                       |
| Card - Amazon Pay               | `cc-amazon-pay`                  |
| Card - Amex                     | `cc-amex`                        |
| Card - Apple Pay                | `cc-apple-pay`                   |
| Card - Diners Club              | `cc-diners-club`                 |
| Card - Discover                 | `cc-discover`                    |
| Card - JCB                      | `cc-jcb`                         |
| Card - Mastercard               | `cc-mastercard`                  |
| Card - PayPal                   | `cc-paypal`                      |
| Card - Stripe                   | `cc-stripe`                      |
| Card - Visa                     | `cc-visa`                        |
| Centercode                      | `centercode`                     |
| CentOS                          | `centos`                         |
| Chrome                          | `chrome`                         |
| Chromecast                      | `chromecast`                     |
| Circle Zulip                    | `circle-zulip`                   |
| Claude                          | `claude`                         |
| Cloudflare                      | `cloudflare`                     |
| Cloudscale                      | `cloudscale`                     |
| Cloudsmith                      | `cloudsmith`                     |
| Cloudversify                    | `cloudversify`                   |
| Cmplid                          | `cmplid`                         |
| Codepen                         | `codepen`                        |
| Codiepie                        | `codiepie`                       |
| Confluence                      | `confluence`                     |
| Connect Develop                 | `connectdevelop`                 |
| Contao                          | `contao`                         |
| Cosmos                          | `cosmos`                         |
| Cotton Bureau                   | `cotton-bureau`                  |
| Cpanel                          | `cpanel`                         |
| Creative Commons                | `creative-commons`               |
| Creative Commons By             | `creative-commons-by`            |
| Creative Commons NC             | `creative-commons-nc`            |
| Creative Commons NC EU          | `creative-commons-nc-eu`         |
| Creative Commons NC JP          | `creative-commons-nc-jp`         |
| Creative Commons ND             | `creative-commons-nd`            |
| Creative Commons PD             | `creative-commons-pd`            |
| Creative Commons Pd Alternative | `creative-commons-pd-alt`        |
| Creative Commons Remix          | `creative-commons-remix`         |
| Creative Commons SA             | `creative-commons-sa`            |
| Creative Commons Sampling       | `creative-commons-sampling`      |
| Creative Commons Sampling Plus  | `creative-commons-sampling-plus` |
| Creative Commons Share          | `creative-commons-share`         |
| Creative Commons Zero           | `creative-commons-zero`          |
| Critical Role                   | `critical-role`                  |
| CSS                             | `css`                            |
| CSS3                            | `css3`                           |
| CSS3 Alternative                | `css3-alt`                       |
| Cuttlefish                      | `cuttlefish`                     |
| D And D                         | `d-and-d`                        |
| D And D Beyond                  | `d-and-d-beyond`                 |
| Dailymotion                     | `dailymotion`                    |
| Dart Lang                       | `dart-lang`                      |
| Dashcube                        | `dashcube`                       |
| Debian                          | `debian`                         |
| Deezer                          | `deezer`                         |
| Delicious                       | `delicious`                      |
| Deploydog                       | `deploydog`                      |
| Deskpro                         | `deskpro`                        |
| Deskpro (square)                | `square-deskpro`                 |
| Dev                             | `dev`                            |
| Deviantart                      | `deviantart`                     |
| DHL                             | `dhl`                            |
| Diaspora                        | `diaspora`                       |
| Digg                            | `digg`                           |
| Digital Ocean                   | `digital-ocean`                  |
| Discord                         | `discord`                        |
| Discourse                       | `discourse`                      |
| Disqus                          | `disqus`                         |
| Dochub                          | `dochub`                         |
| Docker                          | `docker`                         |
| Draft2digital                   | `draft2digital`                  |
| Dribbble                        | `dribbble`                       |
| Dribbble (square)               | `square-dribbble`                |
| Dropbox                         | `dropbox`                        |
| Drupal                          | `drupal`                         |
| Duolingo                        | `duolingo`                       |
| Dyalog                          | `dyalog`                         |
| Earlybirds                      | `earlybirds`                     |
| eBay                            | `ebay`                           |
| Edge                            | `edge`                           |
| Edge Legacy                     | `edge-legacy`                    |
| Elementor                       | `elementor`                      |
| Eleventy                        | `eleventy`                       |
| Ello                            | `ello`                           |
| Ember                           | `ember`                          |
| Empire                          | `empire`                         |
| Envelope                        | `envelope`                       |
| Envelope (square)               | `square-envelope`                |
| Envira                          | `envira`                         |
| Erlang                          | `erlang`                         |
| Ethereum                        | `ethereum`                       |
| Etsy                            | `etsy`                           |
| Evernote                        | `evernote`                       |
| Expeditedssl                    | `expeditedssl`                   |
| Facebook                        | `facebook`                       |
| Facebook (square)               | `square-facebook`                |
| Facebook F                      | `facebook-f`                     |
| Facebook Messenger              | `facebook-messenger`             |
| Fantasy Flight Games            | `fantasy-flight-games`           |
| FedEx                           | `fedex`                          |
| Fediverse                       | `fediverse`                      |
| Fedora                          | `fedora`                         |
| Figma                           | `figma`                          |
| Figma (square)                  | `square-figma`                   |
| Files Pinwheel                  | `files-pinwheel`                 |
| Firefox                         | `firefox`                        |
| Firefox Browser                 | `firefox-browser`                |
| First Order                     | `first-order`                    |
| First Order Alternative         | `first-order-alt`                |
| Firstdraft                      | `firstdraft`                     |
| Flickr                          | `flickr`                         |
| Flipboard                       | `flipboard`                      |
| Flutter                         | `flutter`                        |
| Fly                             | `fly`                            |
| Font Awesome                    | `font-awesome`                   |
| Font Awesome (square)           | `square-font-awesome`            |
| Font Awesome (square, stroked)  | `square-font-awesome-stroke`     |
| Fonticons                       | `fonticons`                      |
| Fonticons Fi                    | `fonticons-fi`                   |
| Forgejo                         | `forgejo`                        |
| Fort Awesome                    | `fort-awesome`                   |
| Fort Awesome Alternative        | `fort-awesome-alt`               |
| Forumbee                        | `forumbee`                       |
| Foursquare                      | `foursquare`                     |
| Free Code Camp                  | `free-code-camp`                 |
| FreeBSD                         | `freebsd`                        |
| Fulcrum                         | `fulcrum`                        |
| Galactic Republic               | `galactic-republic`              |
| Galactic Senate                 | `galactic-senate`                |
| Get Pocket                      | `get-pocket`                     |
| Gg                              | `gg`                             |
| Gg (circle)                     | `gg-circle`                      |
| Git                             | `git`                            |
| Git (square)                    | `square-git`                     |
| Git Alternative                 | `git-alt`                        |
| Gitee                           | `gitee`                          |
| GitHub                          | `github`                         |
| GitHub (square)                 | `square-github`                  |
| GitHub Alternative              | `github-alt`                     |
| Gitkraken                       | `gitkraken`                      |
| GitLab                          | `gitlab`                         |
| GitLab (square)                 | `square-gitlab`                  |
| Gitter                          | `gitter`                         |
| Glide                           | `glide`                          |
| Glide G                         | `glide-g`                        |
| Globaleaks                      | `globaleaks`                     |
| Gofore                          | `gofore`                         |
| Golang                          | `golang`                         |
| Goodreads                       | `goodreads`                      |
| Goodreads G                     | `goodreads-g`                    |
| Google                          | `google`                         |
| Google Drive                    | `google-drive`                   |
| Google Pay                      | `google-pay`                     |
| Google Play                     | `google-play`                    |
| Google Plus                     | `google-plus`                    |
| Google Plus (square)            | `square-google-plus`             |
| Google Plus G                   | `google-plus-g`                  |
| Google Scholar                  | `google-scholar`                 |
| Google Wallet                   | `google-wallet`                  |
| Gratipay                        | `gratipay`                       |
| Grav                            | `grav`                           |
| Gripfire                        | `gripfire`                       |
| Grunt                           | `grunt`                          |
| Guilded                         | `guilded`                        |
| Gulp                            | `gulp`                           |
| Hacker News                     | `hacker-news`                    |
| Hacker News (square)            | `square-hacker-news`             |
| HackerRank                      | `hackerrank`                     |
| Hashnode                        | `hashnode`                       |
| Hips                            | `hips`                           |
| Hire A Helper                   | `hire-a-helper`                  |
| Hive                            | `hive`                           |
| Hooli                           | `hooli`                          |
| Hornbill                        | `hornbill`                       |
| Hotjar                          | `hotjar`                         |
| Houzz                           | `houzz`                          |
| HTML5                           | `html5`                          |
| Hubspot                         | `hubspot`                        |
| Hugging Face                    | `hugging-face`                   |
| Ideal                           | `ideal`                          |
| IMDb                            | `imdb`                           |
| Instagram                       | `instagram`                      |
| Instagram (square)              | `square-instagram`               |
| Instalod                        | `instalod`                       |
| Intercom                        | `intercom`                       |
| Internet Explorer               | `internet-explorer`              |
| Invision                        | `invision`                       |
| Ioxhost                         | `ioxhost`                        |
| Itch Io                         | `itch-io`                        |
| iTunes                          | `itunes`                         |
| iTunes Note                     | `itunes-note`                    |
| Java                            | `java`                           |
| Jedi Order                      | `jedi-order`                     |
| Jenkins                         | `jenkins`                        |
| Jira                            | `jira`                           |
| Joget                           | `joget`                          |
| Joomla                          | `joomla`                         |
| Js                              | `js`                             |
| Js (square)                     | `square-js`                      |
| Jsfiddle                        | `jsfiddle`                       |
| Julia                           | `julia`                          |
| Jxl                             | `jxl`                            |
| Kaggle                          | `kaggle`                         |
| Kakao Talk                      | `kakao-talk`                     |
| Keybase                         | `keybase`                        |
| Keycdn                          | `keycdn`                         |
| Kickstarter                     | `kickstarter`                    |
| Kickstarter K                   | `kickstarter-k`                  |
| Ko Fi                           | `ko-fi`                          |
| Korvue                          | `korvue`                         |
| Kubernetes                      | `kubernetes`                     |
| Laravel                         | `laravel`                        |
| Lastfm                          | `lastfm`                         |
| Lastfm (square)                 | `square-lastfm`                  |
| Leanpub                         | `leanpub`                        |
| LeetCode                        | `leetcode`                       |
| Less                            | `less`                           |
| Letterboxd                      | `letterboxd`                     |
| Letterboxd (square)             | `square-letterboxd`              |
| Line                            | `line`                           |
| Link                            | `link`                           |
| LinkedIn                        | `linkedin`                       |
| LinkedIn (square)               | `square-linkedin`                |
| LinkedIn In                     | `linkedin-in`                    |
| Linktree                        | `linktree`                       |
| Linode                          | `linode`                         |
| Linux                           | `linux`                          |
| Lumon                           | `lumon`                          |
| Lumon Drop                      | `lumon-drop`                     |
| Lyft                            | `lyft`                           |
| Magento                         | `magento`                        |
| Mandalorian                     | `mandalorian`                    |
| Markdown                        | `markdown`                       |
| Mastodon                        | `mastodon`                       |
| MaxCDN                          | `maxcdn`                         |
| Mdb                             | `mdb`                            |
| Medapps                         | `medapps`                        |
| Medium                          | `medium`                         |
| Medrt                           | `medrt`                          |
| Meetup                          | `meetup`                         |
| Megaport                        | `megaport`                       |
| Mendeley                        | `mendeley`                       |
| Meta                            | `meta`                           |
| Microblog                       | `microblog`                      |
| Microsoft                       | `microsoft`                      |
| Mintbit                         | `mintbit`                        |
| Mix                             | `mix`                            |
| Mixcloud                        | `mixcloud`                       |
| Mixer                           | `mixer`                          |
| Mizuni                          | `mizuni`                         |
| Modx                            | `modx`                           |
| Monero                          | `monero`                         |
| Napster                         | `napster`                        |
| Neos                            | `neos`                           |
| Nfc Directional                 | `nfc-directional`                |
| Nfc Symbol                      | `nfc-symbol`                     |
| Nimblr                          | `nimblr`                         |
| Node                            | `node`                           |
| Node.js                         | `node-js`                        |
| Notion                          | `notion`                         |
| Npm                             | `npm`                            |
| Ns8                             | `ns8`                            |
| Nutritionix                     | `nutritionix`                    |
| Obsidian                        | `obsidian`                       |
| Octopus Deploy                  | `octopus-deploy`                 |
| Odnoklassniki                   | `odnoklassniki`                  |
| Odnoklassniki (square)          | `square-odnoklassniki`           |
| Odysee                          | `odysee`                         |
| Old Republic                    | `old-republic`                   |
| OpenAI                          | `openai`                         |
| Opencart                        | `opencart`                       |
| OpenID                          | `openid`                         |
| Openstreetmap                   | `openstreetmap`                  |
| Opensuse                        | `opensuse`                       |
| Opera                           | `opera`                          |
| Optin Monster                   | `optin-monster`                  |
| Orcid                           | `orcid`                          |
| Osi                             | `osi`                            |
| Padlet                          | `padlet`                         |
| Page4                           | `page4`                          |
| Pagelines                       | `pagelines`                      |
| Palfed                          | `palfed`                         |
| Pandora                         | `pandora`                        |
| Patreon                         | `patreon`                        |
| PayPal                          | `paypal`                         |
| Perbyte                         | `perbyte`                        |
| Periscope                       | `periscope`                      |
| Phabricator                     | `phabricator`                    |
| Phoenix Framework               | `phoenix-framework`              |
| Phoenix Squadron                | `phoenix-squadron`               |
| Phone                           | `phone`                          |
| Phone (square)                  | `square-phone`                   |
| PHP                             | `php`                            |
| Pied Piper                      | `pied-piper`                     |
| Pied Piper (square)             | `square-pied-piper`              |
| Pied Piper Alternative          | `pied-piper-alt`                 |
| Pied Piper Hat                  | `pied-piper-hat`                 |
| Pied Piper PP                   | `pied-piper-pp`                  |
| Pinterest                       | `pinterest`                      |
| Pinterest (square)              | `square-pinterest`               |
| Pinterest P                     | `pinterest-p`                    |
| Pix                             | `pix`                            |
| Pixelfed                        | `pixelfed`                       |
| Pixiv                           | `pixiv`                          |
| Playstation                     | `playstation`                    |
| PostgreSQL                      | `postgresql`                     |
| Product Hunt                    | `product-hunt`                   |
| Pushed                          | `pushed`                         |
| Python                          | `python`                         |
| QQ                              | `qq`                             |
| Quinscape                       | `quinscape`                      |
| Quora                           | `quora`                          |
| R Project                       | `r-project`                      |
| Raspberry Pi                    | `raspberry-pi`                   |
| Ravelry                         | `ravelry`                        |
| React                           | `react`                          |
| Reacteurope                     | `reacteurope`                    |
| Readme                          | `readme`                         |
| Rebel                           | `rebel`                          |
| Red River                       | `red-river`                      |
| Reddit                          | `reddit`                         |
| Reddit (square)                 | `square-reddit`                  |
| Reddit Alien                    | `reddit-alien`                   |
| Red Hat                         | `redhat`                         |
| Renren                          | `renren`                         |
| Replyd                          | `replyd`                         |
| Researchgate                    | `researchgate`                   |
| Resolving                       | `resolving`                      |
| Rev                             | `rev`                            |
| Rocketchat                      | `rocketchat`                     |
| Rockrms                         | `rockrms`                        |
| Rust                            | `rust`                           |
| Safari                          | `safari`                         |
| Salesforce                      | `salesforce`                     |
| Sass                            | `sass`                           |
| Scaleway                        | `scaleway`                       |
| Schlix                          | `schlix`                         |
| Screenpal                       | `screenpal`                      |
| Scribd                          | `scribd`                         |
| Searchengin                     | `searchengin`                    |
| Sellcast                        | `sellcast`                       |
| Sellsy                          | `sellsy`                         |
| Servicestack                    | `servicestack`                   |
| Shirtsinbulk                    | `shirtsinbulk`                   |
| Shoelace                        | `shoelace`                       |
| Shopify                         | `shopify`                        |
| Shopware                        | `shopware`                       |
| Signal Messenger                | `signal-messenger`               |
| Simplybuilt                     | `simplybuilt`                    |
| Sistrix                         | `sistrix`                        |
| Sith                            | `sith`                           |
| Sitrox                          | `sitrox`                         |
| Sketch                          | `sketch`                         |
| Skyatlas                        | `skyatlas`                       |
| Skype                           | `skype`                          |
| Slack                           | `slack`                          |
| Slideshare                      | `slideshare`                     |
| Snapchat                        | `snapchat`                       |
| Snapchat (square)               | `square-snapchat`                |
| Solana                          | `solana`                         |
| SoundCloud                      | `soundcloud`                     |
| SourceTree                      | `sourcetree`                     |
| Space Awesome                   | `space-awesome`                  |
| Speakap                         | `speakap`                        |
| Speaker Deck                    | `speaker-deck`                   |
| Spotify                         | `spotify`                        |
| Squarespace                     | `squarespace`                    |
| Stack Exchange                  | `stack-exchange`                 |
| Stack Overflow                  | `stack-overflow`                 |
| StackPath                       | `stackpath`                      |
| Staylinked                      | `staylinked`                     |
| Steam                           | `steam`                          |
| Steam (square)                  | `square-steam`                   |
| Steam Symbol                    | `steam-symbol`                   |
| Sticker Mule                    | `sticker-mule`                   |
| Strava                          | `strava`                         |
| Stripe                          | `stripe`                         |
| Stripe S                        | `stripe-s`                       |
| Stubber                         | `stubber`                        |
| Studiovinari                    | `studiovinari`                   |
| Stumbleupon                     | `stumbleupon`                    |
| Stumbleupon (circle)            | `stumbleupon-circle`             |
| Superpowers                     | `superpowers`                    |
| Supple                          | `supple`                         |
| Supportnow                      | `supportnow`                     |
| Suse                            | `suse`                           |
| Svelte                          | `svelte`                         |
| Swift                           | `swift`                          |
| Symfony                         | `symfony`                        |
| Symfonycasts                    | `symfonycasts`                   |
| Tailwind CSS                    | `tailwind-css`                   |
| TeamSpeak                       | `teamspeak`                      |
| Telegram                        | `telegram`                       |
| Tencent Weibo                   | `tencent-weibo`                  |
| Tex                             | `tex`                            |
| The Red Yeti                    | `the-red-yeti`                   |
| Themeco                         | `themeco`                        |
| Themeisle                       | `themeisle`                      |
| Think Peaks                     | `think-peaks`                    |
| Threads                         | `threads`                        |
| Threads (square)                | `square-threads`                 |
| Threema                         | `threema`                        |
| Tidal                           | `tidal`                          |
| TikTok                          | `tiktok`                         |
| Tor Browser                     | `tor-browser`                    |
| Trade Federation                | `trade-federation`               |
| Trello                          | `trello`                         |
| Tumblr                          | `tumblr`                         |
| Tumblr (square)                 | `square-tumblr`                  |
| Twitch                          | `twitch`                         |
| Twitter                         | `twitter`                        |
| Twitter (square)                | `square-twitter`                 |
| TypeScript                      | `typescript`                     |
| Typo3                           | `typo3`                          |
| Uber                            | `uber`                           |
| Ubuntu                          | `ubuntu`                         |
| UIkit                           | `uikit`                          |
| Ultralytics                     | `ultralytics`                    |
| Ultralytics Hub                 | `ultralytics-hub`                |
| Ultralytics Yolo                | `ultralytics-yolo`               |
| Umbraco                         | `umbraco`                        |
| Uncharted                       | `uncharted`                      |
| Uniregistry                     | `uniregistry`                    |
| Unison                          | `unison`                         |
| Unity                           | `unity`                          |
| Unreal Engine                   | `unreal-engine`                  |
| Unsplash                        | `unsplash`                       |
| Untappd                         | `untappd`                        |
| UPS                             | `ups`                            |
| Upwork                          | `upwork`                         |
| Upwork (square)                 | `square-upwork`                  |
| USB                             | `usb`                            |
| USPS                            | `usps`                           |
| Ussunnah                        | `ussunnah`                       |
| Vaadin                          | `vaadin`                         |
| Venmo                           | `venmo`                          |
| Venmo V                         | `venmo-v`                        |
| Viacoin                         | `viacoin`                        |
| Viadeo                          | `viadeo`                         |
| Viadeo (square)                 | `square-viadeo`                  |
| Viber                           | `viber`                          |
| Vim                             | `vim`                            |
| Vimeo                           | `vimeo`                          |
| Vimeo (square)                  | `square-vimeo`                   |
| Vimeo V                         | `vimeo-v`                        |
| Vine                            | `vine`                           |
| Vk                              | `vk`                             |
| Vnv                             | `vnv`                            |
| Vsco                            | `vsco`                           |
| Vue.js                          | `vuejs`                          |
| W3C                             | `w3c`                            |
| Watchman Monitoring             | `watchman-monitoring`            |
| Waze                            | `waze`                           |
| Web Awesome                     | `web-awesome`                    |
| Web Awesome (square)            | `square-web-awesome`             |
| Web Awesome (square, stroked)   | `square-web-awesome-stroke`      |
| Webflow                         | `webflow`                        |
| Weebly                          | `weebly`                         |
| Weibo                           | `weibo`                          |
| Weixin                          | `weixin`                         |
| WhatsApp                        | `whatsapp`                       |
| WhatsApp (square)               | `square-whatsapp`                |
| WHMCS                           | `whmcs`                          |
| Wikipedia W                     | `wikipedia-w`                    |
| Windows                         | `windows`                        |
| Wirsindhandwerk                 | `wirsindhandwerk`                |
| Wix                             | `wix`                            |
| Wizards Of The Coast            | `wizards-of-the-coast`           |
| Wodu                            | `wodu`                           |
| Wolf Pack Battalion             | `wolf-pack-battalion`            |
| WordPress                       | `wordpress`                      |
| WordPress Simple                | `wordpress-simple`               |
| WPBeginner                      | `wpbeginner`                     |
| WPExplorer                      | `wpexplorer`                     |
| WPForms                         | `wpforms`                        |
| Wpressr                         | `wpressr`                        |
| X/Twitter                       | `x-twitter`                      |
| X/Twitter (square)              | `square-x-twitter`               |
| Xbox                            | `xbox`                           |
| Xing                            | `xing`                           |
| Xing (square)                   | `square-xing`                    |
| XMPP                            | `xmpp`                           |
| Y Combinator                    | `y-combinator`                   |
| Yahoo                           | `yahoo`                          |
| Yammer                          | `yammer`                         |
| Yandex                          | `yandex`                         |
| Yandex International            | `yandex-international`           |
| Yarn                            | `yarn`                           |
| Yelp                            | `yelp`                           |
| Yoast                           | `yoast`                          |
| YouTube                         | `youtube`                        |
| YouTube (square)                | `square-youtube`                 |
| Zhihu                           | `zhihu`                          |
| Zoom                            | `zoom`                           |
| Zulip                           | `zulip`                          |


# Videos
Source: https://loops.so/docs/creating-emails/editor/videos

Email clients do not widely support embedded video. Instead use an image or GIF.

**Video is not widely supported in email clients**, making it hard to embed videos in your emails.

We suggest adding a still image from the video into your email—with a play button overlaid on top if you want—and then add a link to the image pointing to your hosted video.

You can also use GIFs to add a video-like effect to your email. Just make sure that the GIF's file size is small enough to load quickly.


# Font support
Source: https://loops.so/docs/creating-emails/font-support

Font support via Google Fonts is now available in Loops.

## Web font support

Custom font support via Google Fonts is now available in Loops. You can use any font from Google Fonts in your emails.

To use a custom font, create or enable a Theme, then click **Edit**. You will then be able to apply a custom font in the Text styles.

<img alt="Apply custom font" />

Please note that not all fonts are supported by all email clients. See the table below for a list of supported email clients.

| Support | Platform   |
| ------- | ---------- |
| No      | Gmail      |
| No      | Outlook    |
| Yes     | Apple Mail |
| Yes     | Samsung    |
| Yes     | Comcast    |
| No      | AOL        |
| No      | Yahoo      |

## Font fallback chain

When a custom font isn't supported by an email client, Loops uses a fallback chain to ensure your emails still look great. The fallback chain follows this pattern:

`[selected font], [ui fallback by category], [generic category]`

| Category  | Fallback chain                          |
| --------- | --------------------------------------- |
| Sans      | SelectedFont, ui-sans-serif, sans-serif |
| Serif     | SelectedFont, ui-serif, serif           |
| Monospace | SelectedFont, ui-monospace, monospace   |

## General list of email-safe fonts

The following fonts are supported by all modern email clients. They're considered "email safe" fonts because they're available on most devices and email clients.

* Arial
* Courier New
* Georgia
* Lucida Sans Unicode
* Tahoma
* Times New Roman
* Trebuchet MS
* Verdana


# Guardian
Source: https://loops.so/docs/creating-emails/guardian

Sending emails protected by Guardian.

Guardian checks your emails in real time and flags issues *before* you hit send. It catches misconfigured or missing elements in campaigns, workflows, and transactional emails so you ship with confidence.

<img alt="Guardian" />

## Guardian checks

### Misplaced variables

Occasionally, content will be copied between email types (campaign \<> workflow \<> transactional) and certain types of dynamic content may not be supported. Instead of stripping the variables or blocking the paste, Guardian flags the errors so you can make sure the content is adjusted and formatted properly for the email type.

## Save states

We transparently poll for the last saved checkpoint and keep you updated. This is especially helpful in poor network conditions such as hotspots on the go or on a plane. If local changes fall out of sync we'll let you know.

## Missing button links

If you add a button, we assume you would like a link (href) attached. Now we'll flag that for you to block any accidental sends.

## Missing fallback variables

When using contact and event properties in campaigns and workflows, emails will not be sent if the values are missing from the contact or event.

For example, if you have inserted "Hello `First Name`" in your email, and the contact doesn't have a first name value, the workflow or campaign email will not be sent. In some cases, that can be desired behavior but we've found most of the time a fallback should be added. We've added it as a warning and now Guardian will alert you when there are missing fallbacks and emails will send as expected.

<CardGroup>
  <Card title="Fallback values" icon="crystal-ball" href="/creating-emails/personalizing-emails#fallback-values">
    Learn about using fallback values for your dynamic content.
  </Card>
</CardGroup>

## Future improvements

* Content checks
* Boosted spellcheck
* Adhering to design/brand guidelines

<Info>
  Should we add something else? [Let our support team know](https://app.loops.so/settings?page=support).
</Info>


# LMX
Source: https://loops.so/docs/creating-emails/lmx

Author email content with Loops Markup Language (LMX), a structured markup for sections, components, variables, and themed styling in Loops emails.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

LMX (Loops Markup Language) is an XML-based format for writing email content in Loops. Every piece of content is represented by an explicit PascalCase tag.

LMX can be used with the Content API to create and update campaign, workflow, and transactional email messages programmatically.

LMX sits between the API and our editor, making it possible to edit emails across both the editor and API.

You can keep LMX in your codebase and update draft email messages through the
[Content API](/docs/api-reference/update-email-message) or
[email messages CLI](/docs/cli/email-messages). Fetch the current
`contentRevisionId`, edit the LMX in Git, and send the update back with
revision protection.

<CardGroup>
  <Card title="Campaign content examples" icon="code" href="/api-reference/examples/campaigns">
    Create campaigns, query themes and components, and update email messages with LMX.
  </Card>

  <Card title="Update email message API" icon="square-pen" href="/api-reference/update-email-message">
    Send serialized LMX through the Content API for campaign, workflow, and transactional email messages.
  </Card>

  <Card title="Create campaign API" icon="send" href="/api-reference/create-campaign">
    Create a draft campaign and email message before adding LMX content.
  </Card>

  <Card title="Loops CLI" icon="rectangle-terminal" href="/cli">
    Use the CLI for local setup, authentication, and Content API workflows.
  </Card>

  <Card title="Themes API" icon="palette" href="/api-reference/list-themes">
    Find theme IDs and style attributes for the LMX `<Style />` tag.
  </Card>

  <Card title="Components API" icon="box" href="/api-reference/list-components">
    Find reusable components for the LMX `<Component />` tag.
  </Card>
</CardGroup>

## LMX examples

Here are some examples of how to use LMX tags to create emails.

```xml theme={"dark"}
<H1>Hello</H1>
<Paragraph>World</Paragraph>
```

**A list**

```xml theme={"dark"}
<H2>Release checklist</H2>
<OrderedList>
  <ListItem>Finish QA</ListItem>
  <ListItem>Prepare announcement</ListItem>
  <ListItem>Notify teams</ListItem>
</OrderedList>
```

**Two-column layout**

```xml theme={"dark"}
<Columns>
  <ColumnItem>
    <H3>Starter</H3>
    <Paragraph>Best for new projects.</Paragraph>
    <Button href="https://loops.so/pricing">Start free</Button>
  </ColumnItem>
  <ColumnItem>
    <H3>Pro</H3>
    <Paragraph>Advanced automation and analytics.</Paragraph>
    <Button href="https://loops.so/pricing">Upgrade</Button>
  </ColumnItem>
</Columns>
```

**Full campaign example**

```xml theme={"dark"}
<Style themeId="st_default"
  bodyColor="#1F2937"
  bodyYPadding="24" />
<Component componentId="logo" />
<H1>Hi {contact.firstName}, your weekly update is ready</H1>
<Paragraph fontSize="16" lineHeight="150">
  Here is your weekly account summary. Read <Link href="https://loops.so/docs">the docs</Link> for the full changelog.
</Paragraph>
<Button href="https://app.example.com/account/{contact.userId}" align="center" bgColor="#000000" textColor="#ffffff">
  Open dashboard, {contact.firstName}
</Button>
<Divider />
<H2>Top priorities</H2>
<OrderedList>
  <ListItem>Fix onboarding drop-off on step 2</ListItem>
  <ListItem>Publish changelog for API v2</ListItem>
  <ListItem>Review deliverability metrics</ListItem>
</OrderedList>
<Columns gap="24" widths="50,50" verticalAlignment="top">
  <ColumnItem>
    <H3>Docs</H3>
    <Paragraph>Start with the quickstart.</Paragraph>
  </ColumnItem>
  <ColumnItem>
    <Image src="https://images.vialoops.com/cmp2cnlf600yz0f042m792otk/cmp2cz4mn0oru0izztre5fgob.jpg" dynamicSrc="{contact.avatarUrl}" alt="{contact.firstName}" width="180" />
  </ColumnItem>
</Columns>
<Section blockColor="#F8FAFC" blockBorderRadius="12" paddingTop="16" paddingBottom="16">
  <Paragraph>Review your latest activity and <Strong>update your settings</Strong>.</Paragraph>
</Section>
<Paragraph>
  Need help? <Link href="https://example.com/support">Contact support</Link>.
</Paragraph>
<Icons>
  <Icon name="x-twitter" href="https://twitter.com/loops" />
  <Icon name="linkedin" href="https://www.linkedin.com/company/sendwithloops/" />
</Icons>
```

**Workflow email example**

```xml theme={"dark"}
<Style themeId="st_default" bodyColor="#ffffff" backgroundColor="#f3f4f6" bodyYPadding="24" />
<H1>Thanks for your order, {contact.firstName}</H1>
<Paragraph fontSize="16" lineHeight="150">
  Your order total was {event.orderTotal}. We will send updates to {contact.email}.
</Paragraph>
<Button href="https://app.example.com/orders/{event.orderId}" align="center" bgColor="#000000" textColor="#ffffff" borderRadius="12">
  View order
</Button>
```

**Transactional email example**

```xml theme={"dark"}
<Style themeId="st_default" bodyColor="#ffffff" backgroundColor="#f3f4f6" bodyYPadding="24" />
<H1>Reset your password</H1>
<Paragraph fontSize="16" lineHeight="150">
  Hi {data.firstName}, we received a request to reset your password.
</Paragraph>
<Button href="{data.resetLink}" align="center" bgColor="#000000" textColor="#ffffff" borderRadius="12">
  Reset password
</Button>
<Paragraph fontSize="14" lineHeight="150">
  If you did not request this, you can ignore this email.
</Paragraph>
```

## Core rules

* LMX is XML, not HTML or Markdown. Tags are case-sensitive (for example, `<Paragraph>`, not `<paragraph>`).
* LMX is not MJML. Do not use MJML tags (such as `<mj-text>`).
* Do not use the editor's dynamic tag syntax in LMX documents (for example `{EVENT_PROPERTY:orderId}` or `{DATA_VARIABLE:resetLink}` or unprefixed `{firstName}`). Use `{contact.*}` for contact properties, `{event.*}` for workflow event properties, and `{data.*}` for transactional email data variables.
* A document is a sequence of top-level **block tags**, optionally with one top-level `<Style />`.
* Text is not allowed at the top level. Wrap top-level text in a block tag like `<Paragraph>`.
* Top-level inline tags and variables are invalid.
* Required attributes must be present (for example, `<Image />` requires `src`).
* Self-closing tags must end with `/>` (for example, `<Image ... />`, `<Br />`).
* Unknown tags are rejected. Use only documented attributes in public LMX examples.
* Attribute values are quoted strings. Numbers and booleans are passed as strings.
* Whitespace in text content collapses like HTML, except inside `<CodeBlock>`, where interior whitespace is preserved. Use `<Br />` for explicit line breaks.
* Whitespace between block tags is ignored, so pretty-printing is safe.
* Escape text and attributes as needed (`&lt;`, `&amp;`, `&quot;`).

<Note>
  Emails automatically have a footer appended, so you don't need to include your address or unsubscribe link at the bottom of your LMX content.
</Note>

## Source control

You can keep LMX files alongside the rest of your application code (for example, in an `emails/` directory in your repo) and sync them with Loops through the CLI or Content API. LMX is plain text, so it diffs and reviews cleanly in pull requests.

A typical Git-based workflow:

1. **Pull the latest content** before editing. Run [`loops email-messages get <id>`](/docs/cli/email-messages#get) to fetch the current LMX and `contentRevisionId`, and save the LMX to a file in your repo.
2. **Edit the `.lmx` file** locally and commit your changes.
3. **Push back to Loops** with [`loops email-messages update <id>`](/docs/cli/email-messages#update), passing `--expected-revision-id` set to the `contentRevisionId` from step 1 and `--lmx-file ./path/to/email.lmx`. The update is rejected if the email was edited in Loops in the meantime, so you don't overwrite concurrent changes.
4. **Resolve conflicts** by pulling again with `loops email-messages get` and re-applying your edits. Use `--force` instead of `--expected-revision-id` only when you intentionally want to overwrite the server copy.

The owning campaign must be in `Draft` status for updates to apply. The same flow works through the [Update email message API](/docs/api-reference/update-email-message) if you prefer to call the API directly from CI.

## Tags

Top-level tags define the structure of an email.

| Tag                    | Description                     |          Self-closing         |
| ---------------------- | ------------------------------- | :---------------------------: |
| `<Style />`            | Style metadata (top-level only) |              yes              |
| `<H1>`, `<H2>`, `<H3>` | Heading blocks                  |                               |
| `<Paragraph>`          | Paragraph block                 |                               |
| `<Quote>`              | Quote block                     |                               |
| `<CodeBlock>`          | Code block                      |                               |
| `<Button>`             | Button block                    |                               |
| `<Image />`            | Image block                     |              yes              |
| `<Divider />`          | Divider block                   |              yes              |
| `<OrderedList>`        | Numbered list container         |                               |
| `<UnorderedList>`      | Bulleted list container         |                               |
| `<Columns>`            | Multi-column container          |                               |
| `<Component>`          | Component container             | yes, but can contain children |
| `<Section>`            | Section container               |                               |
| `<Icons>`              | Social icon group               |                               |

Child-only structural tags are valid only inside their parent tags.

| Tag            | Parent tag                         | Description              | Self-closing |
| -------------- | ---------------------------------- | ------------------------ | :----------: |
| `<Br />`       | Inline-content blocks              | Line break               |      yes     |
| `<ListItem>`   | `<OrderedList>`, `<UnorderedList>` | List item                |              |
| `<ColumnItem>` | `<Columns>`                        | Column content container |              |
| `<Icon />`     | `<Icons>`                          | Individual icon          |      yes     |

## Tag reference

### Document styles

Use the optional top-level `<Style />` tag to style the email. Use this to apply a [theme](/docs/creating-emails/styles#themes) to your email and add custom styles, just like you would in the [Style panel](/docs/creating-emails/styles#style-panel) in the editor.

Use a combination of `themeId` and other attributes to bring in defaults while also overriding specific styles. You can also style individual elements in your email using tag attributes (for example, `<Button bgColor="#000000">Open account</Button>` overrides `<Style buttonBodyColor="#CCCCCC" />` for that button).

Styles applied in `<Style />` will apply to all content in the email. For example, defining `buttonBodyColor` in the `<Style />` tag will apply to all buttons in the email.

Self-closing tag. At most one per document.

| Attribute               | Notes                                                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `themeId`               | Theme preset ID to load as the base style before applying overrides.                                                                 |
| `backgroundColor`       | Outer email hex background color around the body container.                                                                          |
| `backgroundXPadding`    | Left and right spacing (px) between the outer background and body container.                                                         |
| `backgroundYPadding`    | Top and bottom spacing (px) between the outer background and body container.                                                         |
| `bodyColor`             | Main body container hex background color.                                                                                            |
| `bodyXPadding`          | Left and right inner padding (px) inside the body container.                                                                         |
| `bodyYPadding`          | Top and bottom inner padding (px) inside the body container.                                                                         |
| `bodyFontFamily`        | Font family from [Google Fonts](https://fonts.google.com/) e.g. `"Inter"`, `"Noto Sans"`                                             |
| `bodyFontCategory`      | Fallback font category. Must be one of: `"ui-sans-serif"`, `"ui-serif"`, `"ui-monospace"`, `"sans-serif"`, `"serif"`, `"monospace"`. |
| `borderColor`           | Default hex border color for the body container.                                                                                     |
| `borderWidth`           | Border width (px) for the body container.                                                                                            |
| `borderRadius`          | Corner radius (px) for the body container.                                                                                           |
| `buttonBodyColor`       | Default hex background color applied to buttons.                                                                                     |
| `buttonBodyXPadding`    | Default left and right internal button padding (px).                                                                                 |
| `buttonBodyYPadding`    | Default top and bottom internal button padding (px).                                                                                 |
| `buttonBorderColor`     | Default hex border color for buttons.                                                                                                |
| `buttonBorderWidth`     | Default button border width (px).                                                                                                    |
| `buttonBorderRadius`    | Default button corner radius (px).                                                                                                   |
| `buttonTextColor`       | Default hex text color used in buttons.                                                                                              |
| `buttonTextFontSize`    | Default button text size (px).                                                                                                       |
| `dividerColor`          | Default hex line color used by `<Divider />`.                                                                                        |
| `dividerBorderWidth`    | Default line thickness (px) used by `<Divider />`.                                                                                   |
| `textBaseColor`         | Default hex color for paragraph, quote, and list text.                                                                               |
| `textBaseFontSize`      | Default font size (px) for paragraph, quote, and list text.                                                                          |
| `textBaseLineHeight`    | Default line height (percentage) for body text.                                                                                      |
| `textBaseLetterSpacing` | Default letter spacing (px) applied to body text.                                                                                    |
| `textLinkColor`         | Default hex text color for inline `<Link>`.                                                                                          |
| `heading1Color`         | Default hex text color for `<H1>`.                                                                                                   |
| `heading1FontSize`      | Default font size (px) for `<H1>`.                                                                                                   |
| `heading1LineHeight`    | Default line height (percentage) for `<H1>`.                                                                                         |
| `heading1LetterSpacing` | Default letter spacing (px) for `<H1>`.                                                                                              |
| `heading2Color`         | Default hex text color for `<H2>`.                                                                                                   |
| `heading2FontSize`      | Default font size (px) for `<H2>`.                                                                                                   |
| `heading2LineHeight`    | Default line height (percentage) for `<H2>`.                                                                                         |
| `heading2LetterSpacing` | Default letter spacing (px) for `<H2>`.                                                                                              |
| `heading3Color`         | Default hex text color for `<H3>`.                                                                                                   |
| `heading3FontSize`      | Default font size (px) for `<H3>`.                                                                                                   |
| `heading3LineHeight`    | Default line height (percentage) for `<H3>`.                                                                                         |
| `heading3LetterSpacing` | Default letter spacing (px) for `<H3>`.                                                                                              |

### Headings

Use `<H1>`, `<H2>`, and `<H3>` for headings.

```xml theme={"dark"}
<H1 align="center" fontSize="34" lineHeight="120">
  Welcome to Loops
</H1>
<H2 paddingTop="16" fontSize="24">
  Your weekly summary
</H2>
<H3 blockColor="#F3F4F6" blockBorderRadius="8" paddingLeft="12" paddingRight="12">
  Highlights
</H3>
```

| Attribute           | Type   | Notes                                     |
| ------------------- | ------ | ----------------------------------------- |
| `fontSize`          | number |                                           |
| `lineHeight`        | number | Percentage value such as `"150"`          |
| `align`             | string | `"left"` (default), `"center"`, `"right"` |
| `blockColor`        | hex    |                                           |
| `blockBorderRadius` | number |                                           |
| `paddingTop`        | number |                                           |
| `paddingRight`      | number |                                           |
| `paddingBottom`     | number |                                           |
| `paddingLeft`       | number |                                           |

### Paragraphs

Use `<Paragraph>` for paragraph text.

```xml theme={"dark"}
<Paragraph fontSize="16" lineHeight="160" align="left" paddingBottom="12">
  Thanks for trying Loops. Here is what changed this week.
</Paragraph>
```

| Attribute           | Type   | Notes                                     |
| ------------------- | ------ | ----------------------------------------- |
| `fontSize`          | number |                                           |
| `lineHeight`        | number | Percentage value such as `"150"`          |
| `align`             | string | `"left"` (default), `"center"`, `"right"` |
| `blockColor`        | hex    |                                           |
| `blockBorderRadius` | number |                                           |
| `paddingTop`        | number |                                           |
| `paddingRight`      | number |                                           |
| `paddingBottom`     | number |                                           |
| `paddingLeft`       | number |                                           |

### Quotes

Use `<Quote>` for blockquote content.

```xml theme={"dark"}
<Quote blockColor="#F9FAFB" blockBorderRadius="8" paddingLeft="16" paddingRight="16">
  Great emails feel personal and timely.
</Quote>
```

| Attribute           | Type   | Notes                                     |
| ------------------- | ------ | ----------------------------------------- |
| `fontSize`          | number |                                           |
| `lineHeight`        | number | Percentage value such as `"150"`          |
| `align`             | string | `"left"` (default), `"center"`, `"right"` |
| `blockColor`        | hex    |                                           |
| `blockBorderRadius` | number |                                           |
| `paddingTop`        | number |                                           |
| `paddingRight`      | number |                                           |
| `paddingBottom`     | number |                                           |
| `paddingLeft`       | number |                                           |

### Code blocks

Use `<CodeBlock>` for code blocks.

Note: inline tags and variable parsing are disabled inside code blocks.

```xml theme={"dark"}
<CodeBlock>
  curl -X POST https://app.loops.so/api/v1/events/send
</CodeBlock>
```

| Attribute           | Type   | Notes                            |
| ------------------- | ------ | -------------------------------- |
| `fontSize`          | number |                                  |
| `lineHeight`        | number | Percentage value such as `"150"` |
| `blockColor`        | hex    |                                  |
| `blockBorderRadius` | number |                                  |
| `paddingTop`        | number |                                  |
| `paddingRight`      | number |                                  |
| `paddingBottom`     | number |                                  |
| `paddingLeft`       | number |                                  |

### Buttons

Use `<Button>` for buttons.

Button content may contain plain text and [dynamic content variables](/docs/creating-emails/lmx#dynamic-content) such as `{contact.firstName}`, `{event.orderId}`, or `{data.firstName}`. Inline formatting tags are not allowed inside buttons.

```xml theme={"dark"}
<Button href="https://app.example.com/users/{contact.userId}">
  Open profile, {contact.firstName}
</Button>

<Button href="{data.resetLink}" align="center" bgColor="#000000" textColor="#ffffff">
  Reset password
</Button>
```

| Attribute           | Type    | Required | Notes                                                                                   |
| ------------------- | ------- | :------: | --------------------------------------------------------------------------------------- |
| `href`              | url     |          | The button's URL. May contain [dynamic variables](/docs/creating-emails/lmx#dynamic-content) |
| `align`             | string  |          | `"left"` (default), `"center"`, `"right"`                                               |
| `notrack`           | boolean |          | `"true"` disables [link tracking](/docs/creating-emails/editor/links#link-tracking)          |
| `bgColor`           | hex     |          |                                                                                         |
| `textColor`         | hex     |          |                                                                                         |
| `borderRadius`      | number  |          |                                                                                         |
| `borderWidth`       | number  |          |                                                                                         |
| `borderColor`       | hex     |          |                                                                                         |
| `innerXPadding`     | number  |          | Inner padding of the button content                                                     |
| `innerYPadding`     | number  |          | Inner padding of the button content                                                     |
| `fontSize`          | number  |          |                                                                                         |
| `blockColor`        | hex     |          |                                                                                         |
| `blockBorderRadius` | number  |          |                                                                                         |
| `paddingTop`        | number  |          | Padding of the container block                                                          |
| `paddingRight`      | number  |          | Padding of the container block                                                          |
| `paddingBottom`     | number  |          | Padding of the container block                                                          |
| `paddingLeft`       | number  |          | Padding of the container block                                                          |

### Images

Use `<Image />` for images.

**`src` must be a Loops-hosted image.** Upload images using the [Uploads API endpoints](/docs/api-reference/create-upload) and use the returned URL as `src`.

For an externally hosted dynamic image, see [Dynamic images](#dynamic-images) below.

```xml theme={"dark"}
<Image
  src="https://images.vialoops.com/cmp2cnlf600yz0f042m792otk/cmp2cz4mn0oru0izztre5fgob.jpg"
  alt="Launch banner"
  href="https://example.com/changelog"
  width="560"
  align="center"
  borderRadius="10"
/>
```

#### Dynamic or externally hosted images

If you want a per-recipient image URL, set `dynamicSrc` to a URL containing a variable (for example, `{contact.avatarUrl}`, `{event.avatarUrl}`, or `{data.avatarUrl}`). The image URL should come from a [contact property](/docs/contacts/properties), [event property](/docs/events/properties), or [data variable](/docs/transactional#add-data-variables).

You must also set `src` to a Loops-hosted static placeholder (this image renders in the Loops editor). When the email is generated for each recipient, the resolved `dynamicSrc` value is injected as the image `src`.

For images hosted on an external host, set `src` to a Loops-hosted static placeholder, then set `dynamicSrc` to the external image URL.

Dynamic image URLs must be publicly accessible and use an email-safe image extension like `.jpg` or `.png`.

[More about dynamic images](/docs/creating-emails/editor/images#dynamic-images).

```xml theme={"dark"}
<Image
  src="https://images.vialoops.com/cmp2cnlf600yz0f042m792otk/cmp2cz4mn0oru0izztre5fgob.jpg"
  dynamicSrc="{contact.avatarUrl}"
  alt="{contact.firstName}"
  width="180"
  align="center"
/>
```

| Attribute           | Type    | Required | Notes                                                                                                                                                |
| ------------------- | ------- | :------: | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src`               | url     |    yes   | Loops-hosted image URL from the [Uploads API](/docs/api-reference/create-upload). User-hosted URLs are not supported.                                     |
| `dynamicSrc`        | string  |          | Per-recipient image URL from a contact property, such as `{contact.avatarUrl}`. See [dynamic images](/docs/creating-emails/editor/images#dynamic-images). |
| `notrack`           | boolean |          | `"true"` disables [link tracking](/docs/creating-emails/editor/links#link-tracking)                                                                       |
| `alt`               | string  |          | Alt text                                                                                                                                             |
| `href`              | url     |          | Add a link to the image. May contain [dynamic variables](/docs/creating-emails/lmx#dynamic-content)                                                       |
| `width`             | number  |          | Pixel width, maximum is 600px. Default is full width (600px). If you add padding, width will auto-adjust to be maximum 600px.                        |
| `align`             | string  |          | `"left"` (default), `"center"`, `"right"`                                                                                                            |
| `borderRadius`      | number  |          |                                                                                                                                                      |
| `borderWidth`       | number  |          |                                                                                                                                                      |
| `borderColor`       | hex     |          |                                                                                                                                                      |
| `blockColor`        | hex     |          | Background color of the container block                                                                                                              |
| `blockBorderRadius` | number  |          | Border radius of the container block                                                                                                                 |
| `paddingTop`        | number  |          |                                                                                                                                                      |
| `paddingRight`      | number  |          |                                                                                                                                                      |
| `paddingBottom`     | number  |          |                                                                                                                                                      |
| `paddingLeft`       | number  |          |                                                                                                                                                      |

### Dividers

Use `<Divider />` for divider lines.

Self-closing tag.

```xml theme={"dark"}
<Divider align="center" width="80" borderWidth="1" color="#E5E7EB" />
```

| Attribute           | Type   | Notes                                     |
| ------------------- | ------ | ----------------------------------------- |
| `align`             | string | `"left"` (default), `"center"`, `"right"` |
| `width`             | number | Percentage value                          |
| `borderWidth`       | number |                                           |
| `color`             | hex    | Color of the divider line                 |
| `blockColor`        | hex    |                                           |
| `blockBorderRadius` | number |                                           |
| `paddingTop`        | number |                                           |
| `paddingRight`      | number |                                           |
| `paddingBottom`     | number |                                           |
| `paddingLeft`       | number |                                           |

### Line breaks

Use `<Br />` for line breaks in inline contexts. It is not valid at the top level.

Self-closing tag.

```xml theme={"dark"}
<Paragraph>
  Thanks for reading.<Br />
  See you next week.
</Paragraph>
```

### Lists

Use `<OrderedList>` or `<UnorderedList>` with `<ListItem>` children only.

These tags must contain at least one `<ListItem>`.

| Attribute | Type   | Notes                                        |
| --------- | ------ | -------------------------------------------- |
| `align`   | string | `"left"` (default), `"center"`, `"right"`    |
| `start`   | number | Item number to start at (ordered lists only) |

```xml theme={"dark"}
<OrderedList>
  <ListItem>Review campaign draft</ListItem>
  <ListItem>Send internal preview</ListItem>
</OrderedList>

<UnorderedList>
  <ListItem>Open rate up 12%</ListItem>
  <ListItem>Clicks up 18%</ListItem>
</UnorderedList>
```

### List items

Use `<ListItem>` for list items.

Only valid inside `<OrderedList>` and `<UnorderedList>` tags.

Can only contain inline content.

```xml theme={"dark"}
<UnorderedList>
  <ListItem fontSize="15" lineHeight="150" paddingBottom="8">
    Use clear call-to-action labels.
  </ListItem>
</UnorderedList>
```

| Attribute           | Type   | Notes                            |
| ------------------- | ------ | -------------------------------- |
| `fontSize`          | number |                                  |
| `lineHeight`        | number | Percentage value such as `"150"` |
| `blockColor`        | hex    |                                  |
| `blockBorderRadius` | number |                                  |
| `paddingTop`        | number |                                  |
| `paddingRight`      | number |                                  |
| `paddingBottom`     | number |                                  |
| `paddingLeft`       | number |                                  |

### Columns

Use `<Columns>` for columns.

Must contain between two and four `<ColumnItem>` children. No other tags are allowed.

Nested `<Columns>` tags are not supported.

```xml theme={"dark"}
<Columns gap="16" widths="60,40" verticalAlignment="top" stackOnMobile="true">
  <ColumnItem>
    <H3>What shipped</H3>
    <Paragraph>New audience filters and event export support.</Paragraph>
  </ColumnItem>
  <ColumnItem>
    <Image src="https://images.vialoops.com/cmp2cnlf600yz0f042m792otk/cmp2cz4mn0oru0izztre5fgob.jpg" alt="Product screenshot" width="220" />
  </ColumnItem>
</Columns>
```

| Attribute           | Type    | Notes                                                             |
| ------------------- | ------- | ----------------------------------------------------------------- |
| `gap`               | number  | Space between columns (px)                                        |
| `widths`            | string  | Comma-separated percentages, one for each column, must total 100. |
| `verticalAlignment` | string  | `"top"`, `"middle"`, `"bottom"`                                   |
| `stackOnMobile`     | boolean |                                                                   |
| `reverseOnMobile`   | boolean |                                                                   |
| `blockColor`        | hex     |                                                                   |
| `blockBorderRadius` | number  |                                                                   |
| `paddingTop`        | number  |                                                                   |
| `paddingRight`      | number  |                                                                   |
| `paddingBottom`     | number  |                                                                   |
| `paddingLeft`       | number  |                                                                   |

Use `<ColumnItem>` for column content containers.

Only valid inside a `<Columns>` tag.

**No attributes.**

```xml theme={"dark"}
<Columns>
  <ColumnItem>
    <Paragraph>Left column content</Paragraph>
  </ColumnItem>
  <ColumnItem>
    <Paragraph>Central column content</Paragraph>
  </ColumnItem>
  <ColumnItem>
    <Paragraph>Right column content</Paragraph>
  </ColumnItem>
</Columns>
```

### Components

Use `<Component>` to add a [component](/docs/creating-emails/components).

Can be self-closing, or can contain block elements, which will override the component's default content.

Children are block tags. Nested `<Component>` tags are not supported.

```xml theme={"dark"}
<Component componentId="header-hero" />

<Component componentId="feature-section">
  <H2>Updated override title</H2>
  <Paragraph>This content replaces the component's defaults.</Paragraph>
</Component>
```

| Attribute           | Type   | Required | Notes              |
| ------------------- | ------ | :------: | ------------------ |
| `componentId`       | string |    yes   | The component's ID |
| `blockColor`        | hex    |          |                    |
| `blockBorderRadius` | number |          |                    |
| `paddingTop`        | number |          |                    |
| `paddingRight`      | number |          |                    |
| `paddingBottom`     | number |          |                    |
| `paddingLeft`       | number |          |                    |

### Sections

Use `<Section>` to group related block content in a shared clickable or styled container.

Children are block tags. Nested `<Section>` tags are not supported.

```xml theme={"dark"}
<Section
  href="https://example.com/account/{contact.userId}"
  blockColor="#F8FAFC"
  blockBorderRadius="12"
  paddingTop="16"
  paddingBottom="16"
>
  <H2>Account summary</H2>
  <Paragraph>Review your latest activity and settings.</Paragraph>
</Section>
```

| Attribute           | Type     | Required | Notes                                                                                                        |
| ------------------- | -------- | :------: | ------------------------------------------------------------------------------------------------------------ |
| `href`              | url      |          | Section link URL. May contain [dynamic variables](/docs/creating-emails/lmx#dynamic-content)                      |
| `notrack`           | boolean  |          | `"true"` disables [link tracking](/docs/creating-emails/editor/links#link-tracking)                               |
| `if`                | variable |          | Condition variable. Show the section only when it matches. See [conditional sections](#conditional-sections) |
| `ifOperation`       | string   |          | Comparison operator for `if`. Defaults to `not_empty`. See [conditional sections](#conditional-sections)     |
| `ifValue`           | string   |          | Value to compare against. Required for operators like `equal` or `contains`                                  |
| `blockColor`        | hex      |          |                                                                                                              |
| `blockBorderRadius` | number   |          |                                                                                                              |
| `blockBorderWidth`  | number   |          | Border width in pixels. Only renders when `blockBorderColor` is also set                                     |
| `blockBorderColor`  | hex      |          | Border color. Only renders when `blockBorderWidth` is also set                                               |
| `paddingTop`        | number   |          |                                                                                                              |
| `paddingRight`      | number   |          |                                                                                                              |
| `paddingBottom`     | number   |          |                                                                                                              |
| `paddingLeft`       | number   |          |                                                                                                              |

#### Conditional sections

Use the `if` attribute to show a section only for some recipients. `if` must be a single, prefixed [variable](#dynamic-content) reference, for example `if="{contact.plan}"`, `if="{event.orderId}"`, or `if="{data.isPromo}"`.

By default the section shows when that variable has a value (`not_empty`). Add `ifOperation` to compare it, and `ifValue` when the operator needs something to compare against:

```xml theme={"dark"}
<Section blockColor="#F8FAFC" blockBorderRadius="12" if="{contact.email}" ifOperation="not_contains" ifValue="loops.so">
  <H2>It's your lucky day</H2>
  <Paragraph>Use code LUCKY for 20% off.</Paragraph>
</Section>
```

Supported `ifOperation` values (which apply depends on the variable's type; data variables are always text):

| Type    | Operators                                                                               |
| ------- | --------------------------------------------------------------------------------------- |
| Text    | `not_empty`, `empty`, `equal`, `not_equal`, `contains`, `not_contains`                  |
| Number  | `not_empty`, `empty`, `numeric_equal`, `numeric_not_equal`, `greater_than`, `less_than` |
| Boolean | `not_empty`, `empty`, `true`, `false`                                                   |

`equal`, `not_equal`, `contains`, `not_contains`, `numeric_equal`, `numeric_not_equal`, `greater_than`, and `less_than` require an `ifValue`. The presence operators (`empty`, `not_empty`) and boolean operators (`true`, `false`) ignore it.

The section is included when the condition is true. A missing condition variable never fails the send, it just hides the section, so condition variables are optional at send time.

Notes:

* `if` must be a single variable. Expressions and multiple variables (for example `if="{data.plan} == 'pro'"`) are not supported; use `ifOperation`/`ifValue` instead.
* Unprefixed values such as `if="{plan}"` are invalid. Use `{contact.}`, `{event.}`, or `{data.}`.
* `ifOperation` and `ifValue` are ignored without an `if` variable, and validation rejects an unsupported operator or a value-requiring operator with no `ifValue`.
* Content inside a hidden section is dropped entirely, including any variables it contains.

### Icons

Use `<Icons>` for lists of icons.

Children must be `<Icon />` with no other tags allowed. At least one `<Icon />` child is required (maximum 100).

```xml theme={"dark"}
<Icons align="center" gap="12" size="20" color="#000000">
  <Icon name="x-twitter" href="https://twitter.com/loops" />
  <Icon name="linkedin" href="https://www.linkedin.com/company/sendwithloops/" />
</Icons>
```

| Attribute           | Type   | Notes                                                           |
| ------------------- | ------ | --------------------------------------------------------------- |
| `align`             | string | `"left"` (default), `"center"`, `"right"`                       |
| `gap`               | number | Space between icons (px)                                        |
| `size`              | number | Icon size (px)                                                  |
| `color`             | hex    | `#000000` (black, default), `#808080` (gray), `#FFFFFF` (white) |
| `blockColor`        | hex    |                                                                 |
| `blockBorderRadius` | number |                                                                 |
| `paddingTop`        | number |                                                                 |
| `paddingRight`      | number |                                                                 |
| `paddingBottom`     | number |                                                                 |
| `paddingLeft`       | number |                                                                 |

Use `<Icon />` for individual icons.

Self-closing tag. Only valid inside an `<Icons>` tag.

| Attribute | Type    | Required | Notes                                                                                           |
| --------- | ------- | :------: | ----------------------------------------------------------------------------------------------- |
| `name`    | string  |    yes   | Icon name (available icons listed [here](/docs/creating-emails/editor/social-icons#available-icons)) |
| `href`    | url     |          | A URL for the icon                                                                              |
| `notrack` | boolean |          | `"true"` disables [link tracking](/docs/creating-emails/editor/links#link-tracking)                  |

## Inline tags

Inline tags format text content inside block elements.

| Tag                 | Description                       |
| ------------------- | --------------------------------- |
| `<Strong>`          | Bold text                         |
| `<Em>`              | Italic text                       |
| `<Underline>`       | Underlined text                   |
| `<Code>`            | Inline code                       |
| `<Strike>`          | Strikethrough text                |
| `<Text>`            | No format, useful for `textColor` |
| `<Link href="...">` | Link with URL                     |

Inline tags are valid only inside inline-content blocks such as `<Paragraph>`, headings, `<Quote>`, and `<ListItem>`.

```xml theme={"dark"}
<Paragraph>
  <Strong textColor="#111827">Important:</Strong>
  <Em textColor="#4B5563"> review your campaign settings</Em>
  <Underline textColor="#111827"> before sending</Underline>.
  Use <Code textColor="#7C3AED">audience_filters</Code> for targeting,
  remove outdated copy with <Strike textColor="#6B7280">old messaging</Strike>,
  and keep defaults with <Text textColor="#374151">plain text</Text>.
  Read more in
  <Link href="https://loops.so/docs/creating-emails" notrack="true"> the docs</Link>.
</Paragraph>
```

### Inline tag attributes

| Tag           | Attribute   | Type    | Required | Notes                                                                          |
| ------------- | ----------- | ------- | :------: | ------------------------------------------------------------------------------ |
| `<Strong>`    | `textColor` | hex     |          |                                                                                |
| `<Em>`        | `textColor` | hex     |          |                                                                                |
| `<Underline>` | `textColor` | hex     |          |                                                                                |
| `<Code>`      | `textColor` | hex     |          |                                                                                |
| `<Strike>`    | `textColor` | hex     |          |                                                                                |
| `<Text>`      | `textColor` | hex     |          |                                                                                |
| `<Link>`      | `href`      | url     |    yes   | May contain [dynamic variables](/docs/creating-emails/lmx#dynamic-content)          |
| `<Link>`      | `notrack`   | boolean |          | `"true"` disables [link tracking](/docs/creating-emails/editor/links#link-tracking) |

## Dynamic content

You can add dynamic content by using a prefixed tag syntax, for example `{contact.firstName}` or `{event.orderTotal}`.

Dynamic tags can be added to [email message](/docs/api-reference/update-email-message) `lmx` bodies and also in other fields like `subject`, `previewText`, `fromName`, `fromEmail` and `replyToEmail`.

Campaign content uses **contact properties**. Workflow email content uses **contact and event properties**. Transactional email content uses **data variables**.

<Warning>
  Unprefixed placeholders like `{firstName}` or `{resetLink}` are not valid in LMX. Neither are other Loops variable prefixes used in MJML or the editor, including `{EVENT_PROPERTY:...}` and `{DATA_VARIABLE:...}`.

  These placeholders are still valid in [MJML emails](/docs/creating-emails/uploading-custom-email) and in the [Loops editor](/docs/creating-emails/editor). In the future, all variables will require a prefix.
</Warning>

| Syntax        | Variable type                                      | Use case                              | Example               |
| ------------- | -------------------------------------------------- | ------------------------------------- | --------------------- |
| `{contact.*}` | [Contact property](/docs/contacts/properties)           | Campaign and workflow personalization | `{contact.firstName}` |
| `{event.*}`   | [Event property](/docs/events/properties)               | Workflow email personalization        | `{event.orderTotal}`  |
| `{data.*}`    | [Data variable](/docs/transactional#add-data-variables) | Transactional email personalization   | `{data.resetLink}`    |

### Contact properties

Use `{contact.<apiName>}` for campaign and workflow personalization, for example `{contact.firstName}`.

Default contact properties include `firstName`, `lastName`, `email`, `notes`, `source`, `userGroup`, `userId`, `subscribed`, and `createdAt`.

```xml theme={"dark"}
<H1>Hi {contact.firstName}, your weekly update is ready</H1>
<Paragraph>Your account ID is {contact.userId}.</Paragraph>
```

### Event properties

Use `{event.<apiName>}` in workflow emails. Event properties come from the event that triggered the workflow.

Property names must match the API names defined in [Events settings](https://app.loops.so/settings?page=events). You must configure fallbacks for event properties in your emails, either with `eventPropertiesFallbacks` when using the [Content API](/docs/api-reference/update-email-message), or in the editor side panel.

```xml theme={"dark"}
<H1>Thanks for your order, {contact.firstName}</H1>
<Paragraph>Your order total was {event.orderTotal}.</Paragraph>
<Button href="https://app.example.com/orders/{event.orderId}">View order</Button>
```

```json Send event payload theme={"dark"}
{
  "email": "user@example.com",
  "eventName": "orderPlaced",
  "eventProperties": {
    "orderTotal": "$49.00",
    "orderId": "ord_123"
  }
}
```

Event property rules:

* Only valid in workflow email LMX. Using `{event.*}` in campaign or transactional emails fails validation.
* Names are case-sensitive and must match the event property API name.
* Configure fallback values in the Loops editor or via `eventPropertiesFallbacks` in the Content API.

### Data variables

Use `{data.<variableName>}` in transactional email LMX. The name after `data.` must match a key in the `dataVariables` object when [sending the email](/docs/api-reference/send-transactional-email).

```xml LMX content theme={"dark"}
<H1>Reset your password, {data.firstName}</H1>
<Paragraph>Click the button below to choose a new password.</Paragraph>
<Button href="{data.resetLink}" align="center">Reset password</Button>
```

```json Send transactional email payload theme={"dark"}
{
  "transactionalId": "cll42l54f20i1la0lfooe3z12",
  "email": "user@example.com",
  "dataVariables": {
    "firstName": "Alex",
    "resetLink": "https://app.example.com/reset/abc123"
  }
}
```

Transactional data variable rules:

* Names are case-sensitive. `{data.resetLink}` and `{data.ResetLink}` are different variables.
* Names may contain only letters, numbers, underscores, and dashes.
* API values may be `string` or `number`.
* Non-optional variables must be present in `dataVariables` or the send fails.
* Optional variables may be omitted from the request or sent as an empty string `""` (`null` does not work). Mark a data variable as optional in the [Loops editor](/docs/transactional#optional-data-variables).

### Where variables are allowed

Variables are allowed in:

* Inline content (headings, paragraphs, quotes, list items, and inline tags)
* Button text
* Supported dynamic attributes: `Button` `href`, `Link` `href`, `Image` `alt`, `Image` `href`, `Image` `dynamicSrc`, and `Section` `href`
* `Section` `if`, which takes a whole variable reference and no surrounding text (see [conditional sections](#conditional-sections))

Variables are not allowed:

* At the top level
* Inside `<CodeBlock>` (braces are literal text)
* In unsupported attributes such as `Image` `src`

### Variables in attributes

Only these attribute values may include variables:

* `Button` `href`
* `Image` `alt`, `href`, and `dynamicSrc`
* `Link` `href`
* `Section` `href`
* `Section` `if` (the whole value must be a single prefixed variable)

`Image` `src` must be a Loops-hosted static URL with no variables. User-hosted URLs are not supported in `src`. Putting a variable like `{contact.avatarUrl}` in `src` is not supported and validation returns `unsupported_dynamic_attr`. When the image URL should vary per recipient (including externally hosted URLs), set `dynamicSrc` to a variable such as `{contact.avatarUrl}`, `{event.avatarUrl}`, or `{data.avatarUrl}` and keep `src` as a Loops-hosted placeholder; at send time, the resolved `dynamicSrc` value replaces `src`.

### Fallback values

LMX does not support inline fallback syntax in variable references.

These forms are **not valid** in LMX: `{contact.firstName|there}`, `{event.orderTotal|0}`, `{contact.firstName:there}`, `{contact.firstName ?? "there"}`, and `fallback="there"` on variable attributes.

Use plain variable references in LMX (for example, `{contact.firstName}`, `{event.orderTotal}`, or `{data.firstName}`). For contact and event properties, configure fallback values through the Loops editor or Content API. For transactional emails, mark a data variable as optional in the editor instead of inventing inline fallback syntax.

## Nesting rules

LMX validates structure, not just syntax. A few key rules:

* Root-level content must use supported top-level block tags (`H1`, `H2`, `H3`, `Paragraph`, `Quote`, `CodeBlock`, `Button`, `Image`, `Divider`, `OrderedList`, `UnorderedList`, `Columns`, `Component`, `Section`, `Icons`, `Style`).
* Inline tags cannot appear at the top level.
* `<ListItem>` and `<Quote>` only accept inline content.
* `<Columns>` can only contain `<ColumnItem>` tags.
* `<ColumnItem>` can contain block content, but not nested `<Columns>` or other `<ColumnItem>` tags.
* `<Style />` is metadata and is only allowed at the top level.

If a tag, attribute, or nesting pattern is invalid, validation issues are returned so problems can be fixed before sending.


# Personalizing emails
Source: https://loops.so/docs/creating-emails/personalizing-emails

Add context and personalization to emails.

Every email you send from Loops can contain dynamic content connected to the contact you're sending to or the event that triggered the email.

A common example of using dynamic content is to personalize an email greeting by using a contact's first name, or by including a customer's plan details in a subscription reminder.

## Types of dynamic content

The three [email types](/docs/types-of-emails) in Loops have different dynamic content available to them:

* **Campaign emails** can contain **contact properties**.
* **Workflow emails** can contain **contact properties** and **event properties**.
* **Transactional emails** can contain **data variables**.

The three types of dynamic content are:

* **Contact properties** are pieces of data related to each [contact](/docs/contacts/properties) in your audience. There are a set of default properties like name and source, but you can also add any number of custom contact properties. If you sync contact data to Loops with the API, an integration or with CSV uploads, you can include that data in your email.
* **Event properties** are pieces of data that can be sent along with every [event](/docs/events) (which are used to trigger workflow emails) via integrations or API calls.
* **Data variables** are pieces of data included in [transactional emails](/docs/transactional), which are populated in the API call. You can utilize [optional data variables](/docs/transactional#optional-data-variables) to make your transactional emails more dynamic.

## Add dynamic content to emails

<Warning>
  If you want to add dynamic content to a [custom MJML
  email](/docs/creating-emails/uploading-custom-email), check the [Dynamic tag
  syntax](#dynamic-tag-syntax) section below.
</Warning>

To add dynamic content to your emails, type "\{" anywhere in the editor or click the `{}` dynamic content icon in the email editor toolbar. Using either of these options will open the dynamic content menu, a filterable list of all available properties you can insert.

<Tip>
  To add dynamic content to button text, you need to manually type the tag syntax directly into the button's URL field. See [Dynamic tag syntax](#dynamic-tag-syntax) below.
</Tip>

<img alt="Dynamic content menu appearing after typing the opening brace" />

<img alt="Adding a contact property" />

These two options are also available in all of the sending setting fields above the editor. An icon appears on the right when you hover over each field, or you can start typing "\{".

<img alt="Adding a contact property to sending details" />

Depending on whether you're editing a campaign, workflow or transactional email, the menu shows different content based on your email type (explained above).

To use the dynamic content menu:

1. Type `{` or hit the `{}` icon in any editor field
2. Continue typing to filter results: `{fir` shows "firstName"
3. Use `↑/↓` to navigate, `Enter` to select, `Escape` to close

When the email is sent, the dynamic content will be replaced with actual values from the contact, event or data variable.

Once you've selected something from the menu in a campaign or workflow email, you will be prompted to enter a fallback value. This ensures emails still send even when data is missing.

<Tip>
  If you send an email and the dynamic content is missing a value, the email
  will not be sent. Make sure to add fallback values to avoid missed sends.
  [Read more below](#fallback-values).
</Tip>

## Fallback values

Fallback values are important for campaign and workflow emails. If a contact doesn't have a value for a property, or an event property is missing, the email won't send.

When you insert a contact or event property using the dynamic content menu, you'll be prompted to enter a fallback. This default text ensures your email still sends even when data is missing.

<img alt="Fallback input appearing after selecting a property" />

Common fallback examples:

| Example                    | Fallback     | Result                  |
| :------------------------- | :----------- | :---------------------- |
| Hey `{firstName}`          | there        | Hey there               |
| Welcome to `{companyName}` | your company | Welcome to your company |

This keeps your emails feeling personalized even when specific data isn't available.

Your fallback is saved and will auto-fill if you insert the same property again.

You can edit the fallback value later on from the editor panel.

<img alt="Setting a fallback value for a dynamic field" />

Fallback values cannot be inserted inline inside tags; they must be added in the editor side panel, or defined separately in API calls.

## Data variables in transactional emails

In transactional emails, dynamic tags work slightly differently: you can create new data variables in your email as you type.

<img alt="Creating a new data variable with the dynamic content menu" />

1. Type `{orderTotal` (using a new name that doesn't exist).
2. Select **Create "orderTotal"** from the menu or press `Enter`.
3. The variable is created and you can edit it from the editor panel (change the name or change to an [optional variable](/docs/transactional#optional-data-variables)).

## Dynamic tag syntax

As well as using the dynamic content menu, you can write dynamic content "tags" directly in the email body. This is especially useful when [uploading custom emails](/docs/creating-emails/uploading-custom-email).

These tags are the only way to add dynamic content in [custom MJML emails](/docs/creating-emails/uploading-custom-email), and to add dynamic content to button text in the editor.

### Contact properties

If you have a custom contact property named `teamName` that you want to add to a campaign, you can write it surrounded by curly brackets in the email:

```
{teamName}
```

When the email is sent, the `teamName` value for each contact will be added to the email.

<img alt="Example merge tag inserted in an email" />

For a list of all of your contact properties, visit the [API Settings](https://app.loops.so/settings?page=api) page. The **API Name** is the name you use within the brackets in your email, for example `{firstName}`, `{lastName}`, `{email}`, etc.

<img alt="Contact properties table" />

### Event properties

To add dynamic data to workflow emails using event properties, the tag requires an `EVENT_PROPERTY:` prefix:

```
{EVENT_PROPERTY:firstName}
```

### Data variables

To add data variables in transactional emails, the tag requires a `DATA_VARIABLE:` prefix:

```
{DATA_VARIABLE:firstName}
```

<Tip>
  It is important to use the right names in your tags. Use the camelCase “API
  Name” from [API settings](https://app.loops.so/settings?page=api) for contact
  properties and event property names from the [Events
  settings](https://app.loops.so/settings?page=events) page. If you have any
  questions about how to format your tags, reach out to us!
</Tip>

## Conditional content

You can add [conditional sections](/docs/creating-emails/editor/sections#conditional-sections) to emails, which allows you to show or hide content based on the value of a contact property, event property or data variable.


# Sending settings
Source: https://loops.so/docs/creating-emails/sending-settings

Set sender name, from/reply, subject/preview, and CC/BCC for emails you send with Loops.

You can set sending settings on every email you send.

There are a set of fields above the email editor, including **From** and **Reply** addresses, **Subject**, **Preview**, **CC** and **BCC**.

Click the `>` button to reveal all of the fields.

<img alt="Email sending settings" />

## Subject and Preview

These two fields let you specify the text that is shown in email lists in email clients.

The **Subject** field should be short and concise. We recommend using a subject line of less than 50 characters and 10 words. This helps to get your point across while also keeping within cut-offs for different email clients.

The **Preview** field can be used as a sub-title or introduction to your email. It is typically displayed next to or below your subject in email clients.

Note that some email clients do not display previews or offer settings for users to hide previews.

If you do not add a preview to your email, sometimes the beginning of your email content will be displayed instead.

## Name, From and Reply

<img alt="Sending settings" />

These fields are used to populate the **From** and **Reply** fields in email clients. They are information fields, helping recipients know who sent the email.

**Name** should be the sender name (person or entity) and the **From** field is used to make up the sending address (it is combined with your sending domain).

The **Reply** field is optional but can be used to specify an email address that you want replies sent to. When a recipient clicks "Reply" in their email client, this email address will be inserted into the **To** field automatically.

If **Reply** is left blank, replies will be sent to the **From** address instead.

<Note>
  When you create a new email, we automatically inherit the most recently used email address in the **From** field to save you time.
</Note>

## CC and BCC

For transactional emails and workflows, you have the option to add a single CC and BCC address, which will send duplicates of each email to the addresses you specify.

By default, you can add a maximum of 1 CC address. [Contact us](mailto:help@loops.so) if you need to increase this limit. BCC is always limited to 1 address.

Make sure to first enable this option in your [Sending settings](https://app.loops.so/settings?page=sending) to enable these fields in the email editor.

<img alt="CC/BCC field setting settings" />

You can then add a CC or BCC to workflow and transactional emails in your account from the sending settings.

<img alt="CC and BCC fields" />

## Dynamic content

You can include [dynamic variables](/docs/creating-emails/personalizing-emails) in every sending settings field, which lets you personalize them for each recipient. This works for all of your campaigns, workflows and transactional emails.

Just click the contact property icon (campaigns), event property icon (workflows) or data variable icon (transactional emails) that appears when you hover over each field.

<img alt="Adding dynamic data to the sending settings" />

### Workflow example

In a workflow email, you can use [event properties](/docs/events/properties) to add dynamic content to fields.

For example, you could insert a `productName` property into the **Subject** field:

> Thank you for your purchase of `productName`!

Then when sending the event to trigger the workflow, include `productName`:

```json {8} theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "creditWarning",
  "eventProperties": {
    "creditsPurchased": 1000,
    "creditsUsed": 903,
    "accountName": "Loops",
    "productName": "Pro plan"
  }
}
```

<CardGroup>
  <Card title="Learn about Events" icon="bolt" href="/events">
    Find out more about triggering workflows with events.
  </Card>
</CardGroup>

### Transactional email example

In a transactional email, an example would be a `replyTo` variable added into the **Reply** field, which would let you set a specific reply email address for every email.

To send this data over the API, add a `replyTo` attribute to your transactional payload:

```json {7} theme={"dark"}
{
  "transactionalId": "clfq6dinn000yl70fgwwyp82l",
  "email": "test@example.com",
  "dataVariables": {
    "name": "Chris",
    "passwordResetLink": "https://example.com/reset-password",
    "replyTo": "chris@loops.so"
  }
}
```

<CardGroup>
  <Card title="Transactional email guide" icon="file-code" href="/transactional">
    Read our guide for sending transactional email with Loops.
  </Card>
</CardGroup>


# Styling emails
Source: https://loops.so/docs/creating-emails/styles

Learn how to edit emails in the Loops editor.

## Styled vs Plain emails

Loops offers two different email styling options, Styled and Plain.

<img alt="Plain vs styled email example" />

By default new emails are created as **Styled** emails in the editor. This option offers a range of design options that can be used to design the base theme for your email, like text and background colors, padding and borders.

A Styled email layout is a responsive central column with a maximum width of 600 pixels.

When using Styled emails you can also create **Themes**, which unlock more editing options like fonts, plus the ability to set consistent, reusable formatting across multiple elements in your email.

**Plain** emails are full-width emails with no base design options (though you can always format individual content using the editor panel). Plain emails better mimic emails sent from clients like Gmail and Apple Mail.

## Style panel

Both Plain and Styled emails will show an editor side panel with formatting options for elements in your email.

This includes all text, headings, links, buttons and dividers in your email, as well as design options for the main email body (padding, background and border).

<img alt="Styled email example in the Loops editor" />

The editor panel changes depending on the currently selected element in your email. For example, if you click on a button, the button formatting options are shown.

There are helpful "Reset to default" icons if you ever want to return styling back to the defaults.

<img alt="Reset style button in the email editor" />

### Language selector

We've included a helpful language selector in the style panel. Using this will tell email clients which language your emails are written in. (It will not translate your emails.)

<Note>
  You can [translate emails inside
  Loops](/docs/creating-emails/translating-emails-with-ai).
</Note>

This increases the accessibility of your emails, plus some email clients may use this language information to auto-translate email content or offer translation options.

The language selector does not have a default value. We recommend selecting the correct language for each of your emails.

<img alt="Language selection menu" />

If you send campaigns and workflows in different languages, we recommend using a [custom contact property](/docs/contacts/properties#custom-contact-properties) to store language information for each contact. Then, when it comes to sending emails, create emails in different languages (using the language switcher to specify the language), and [filter your audience](/docs/contacts/filters-segments#audience-filters) for each email to match the language stored for each contact.

For transactional emails, create a new email in each required language. You will need to store language information for each recipient in your application and map transactional IDs to their respective language groups.

## Themes

Themes let you create repeatable styles that can be easily applied to new emails.

Once applied, a theme can be tweaked on each email and changes will also be synced back to the main style set. This makes the same change happen on any other email that the theme is applied to.

<Note>Themes are not available on Plain mode emails.</Note>

### Create a theme

To create a theme, click the `+` plus icon in the **Theme** editor panel.

<Tip>
  You can also create and manage themes with [the API](/docs/api-reference/create-theme).
</Tip>

<img alt="Creating a new style in Loops" />

Give your theme a name and click the `✓` check icon or press Enter. This will create a theme based on the current styling options selected in the style panel and apply it to the current email.

<img alt="Saving a style in Loops" />

You can now also add this theme to other emails.

### Apply a theme to an email

To add an existing theme to an email, click the **Select** dropdown in the **Theme** editor panel and select your desired theme.

<img alt="Selecting a saved style" />

### Edit a theme

To edit a theme, first open an email with it applied, or apply it to an email. Then click **Edit** to open the theme editor panel.

<img alt="Editing a saved style" />

When individual styles are changed, you'll notice they are highlighted in purple, which denotes styles that have deviated from the main theme.

<img alt="Preview of an edited style" />

A purple **Update** button also appears. Clicking this button updates the theme and also *applies the changes to all emails that have the theme applied*. This makes it really easy to change styles across many emails at once.

### Make one-off changes to a theme

You are free to make one-off custom design changes to an individual email. If you avoid clicking the **Update** button shown above, changes will not be synced to all other emails using the theme.

Any subsequent updates made to the underlying theme will be synced to your email but your individual style changes made directly on the email will override any synced changes.

### Reset to a theme's defaults

After making design changes to an email with a theme applied, you can get back to the theme's defaults by using the `⤺` reset icons.

You can reset styles for each of the **Email styles**, **Text**, **Button** and **Divider** sections individually.

Or if you want to reset all current edits, hover over the **Editing theme** panel at the top and click the reset button there.

This will re-apply the theme's original design to your email and remove *all* current overrides.

<img alt="Resetting changes to a saved style" />

### Remove a theme from an email

To remove a theme from an email click on the `–` minus icon in the **Theme** editor panel.

This will revert back to any styles directly applied to the email before the theme was applied.

<img alt="Removing a saved style" />

### Duplicate a theme

To create a new theme based on the current one, you can duplicate by clicking the `•••` menu icon and selecting **Duplicate**.

<img alt="Duplicating a saved style" />

Give your new theme a new name then click the `✓` check icon or press Enter to save.

This will create a new theme using the current styles and apply it to your email.

### Delete a theme

To delete a theme from your account, make sure the theme is applied to the email you have open and then click the `•••` menu icon and select **Delete**.

To make sure there are no unintended design changes to emails in your account, the settings from the deleted theme will get applied individually to each email it was active on.

However, please note that any overrides made to the theme in the affected emails will still be retained even after the style is deleted.


# Translating emails
Source: https://loops.so/docs/creating-emails/translating-emails-with-ai

Loops provides an automated way to translate your emails.

Translation inside of Loops is a simple, single-click method of translating emails from one language to another entirely inside of the platform.

<Note>
  Enable this feature in [Settings → Sending](https://app.loops.so/settings?page=sending).
</Note>

1. Create a branch, ensuring it contains at least one email to be translated.
2. Click the **language icon** above the branch and choose the **output** language you would like to translate the branch into.
3. The branch will be duplicated in its entirety, including all emails, filters, and timers then the emails within that branch will begin translation.
4. While translating, the emails will display a `Pending` label.
5. When the translation is complete, the emails will display a `Complete` label on the node.

<img alt="Llm Branch Translation" />

### Supported elements

We translate all text nodes in an email, but we do carve out exceptions for certain types of content and use cases.

**We translate:**

* Subject
* Preview
* Headers
* Plain text
* Button text
* From name
* Link text
* Alt text on images
* Quotes
* Dynamic content fallbacks

**We do not translate**

* Link URLs
* Plain text urls
* Email addresses
* Text inside of images
* Dynamic content
  * Data variables
  * Contact properties
  * Event properties
* Brand keywords


# Uploading a custom email
Source: https://loops.so/docs/creating-emails/uploading-custom-email

Use Loops with Emailify, Email Love or MJML.

<Tip>
  While we do support importing MJML, we recommend using our editor or [the API](/docs/api-reference/intro) with [LMX](/docs/creating-emails/lmx) to create your emails. It's the easiest and most reliable way to create beautiful emails that work across all email clients.
</Tip>

## Overview

We allow uploading of custom-coded emails created using a markup language called MJML.

You can upload custom MJML for [all three types of emails](/docs/types-of-emails) in Loops (campaigns, workflows and transactional emails).

When uploading custom email, make sure the email is in a `.zip` file with the following folder structure:

```bash yourEmail.zip theme={"dark"}
index.mjml
img
	|_img1.png
	|_img2.png
```

<Info>Important: Please make sure the MJML file is titled `index.mjml`</Info>

**MJML**

MJML is a markup language that helps you create responsive emails. It's an established framework that helps you create beautiful emails that work across all email clients. You can read more about it at [mjml.io](https://mjml.io/)

**Figma** (via the Emailify and Email Love plugins)

Sometimes you just want that special touch to an email that only Figma can give you. We get it! That's why we support plugins [Emailify](https://www.figma.com/community/plugin/910671699871076601/emailify-html-email-builder) and [Email Love](https://www.figma.com/community/plugin/1387891288648822744/email-love-html-email-builder), which help you create beautiful emails in Figma then export into Loops.

## MJML

If you have your MJML ready, there is one step you need to complete before you can upload it into Loops...

### Add an unsubscribe URL

In your email code, you have to insert an unsubscribe link. This keeps you compliant with email sending restrictions.

All you need to do is add a `{unsubscribe_link}` tag into your MJML. When the email is sent, we will insert a contact-specific URL into this tag, which the contact can click to unsubscribe.

```HTML theme={"dark"}
<a href="{unsubscribe_link}">Unsubscribe</a>
```

### Add dynamic content

You may want to add dynamic content (for example, a contact property or event property) into your email. You can do this using dynamic content tags.

When the email is sent, Loops will replace this tag with the actual value.

<Note>
  Remember to use camelCase format when typing your event property tags.
</Note>

For example, adding a first name [contact property](/docs/contacts/properties) (in campaigns and workflows) can be added like this:

```
Hello {firstName},
```

In workflows triggered by events you can add [event properties](/docs/events/properties) with an `EVENT_PROPERTY:` prefix:

```
Hello {EVENT_PROPERTY:firstName},
```

In transactional emails you can add [data variables](/docs/transactional#add-data-variables) with a `DATA_VARIABLE:` prefix:

```
Hello {DATA_VARIABLE:firstName},
```

[Read more about dynamic content tags](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax)

### Add arrays

You can add [arrays](/docs/creating-emails/editor#arrays) to your email by using the `<loops-array>` tag.

The `variable` attribute is the name of the data variable you want to use for your array.

The `variable-type` attribute should be `DATA_VARIABLE` for transactional emails.

Inside the array tag you can add content, which will be repeated for each item in the array.

You can add dynamic content to the item by using our [dynamic tag syntax](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax). You must use a prefix like `items[].` for array-scoped data variables, where `items` matches the `variable` attribute of your array tag.

This example has one array-scoped data variable `items[].price` (only available within the array) and one "global" data variable `userId` (available throughout the email).

```html theme={"dark"}
<loops-array variable="items" variable-type="DATA_VARIABLE">
  <mj-text>
    {DATA_VARIABLE:items[].price} - {DATA_VARIABLE:userId}
  </mj-text>
</loops-array>
```

## Emailify

Using Emailify, you can create well-designed emails inside Figma, then easily export them ready for Loops.

Download the free [Emailify plugin for Figma](https://www.figma.com/community/plugin/910671699871076601/emailify-html-email-builder) and launch it.

<img alt="Emailify plugin in Figma" />

### Add blocks to your email

To build your email you can drag and drop pre-made blocks provided by Emailify.

Once added you can customize each block.

<img alt="Emailify blocks panel" />

<Note>
  Loops will automatically host all of your email images so they
  can be reliably displayed to your audience. This feature results in an odd
  quirk that you should be aware of:

  Any text in your email that matches the path to one of your images will be replaced with the new path provided by Loops.

  **For example:** `img/myImage.jpg` will be replaced with something like: `https://something.com/lkjn98s08hbAF/img/lkwekHBlhk78kasj.jpg`

  For most situations this won't be an issue, only text that exactly matches the path to one of your images will be replaced.
</Note>

### Add dynamic content

You may want to add dynamic content (for example, a contact property) into your email. You can do this using dynamic content tags.

For example, adding a first name value can be added like this:

```
Hello {firstName},
```

In workflows you can also add event properties with an `EVENT_PROPERTY:` prefix:

```
Hello {EVENT_PROPERTY:firstName},
```

In transactional emails you can add data variables with a `DATA_VARIABLE:` prefix:

```
Hello {DATA_VARIABLE:firstName},
```

[Read more about dynamic content tags](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax)

### Add the Loops footer

While creating your email, you need to include an unsubscribe link.

To do this manually, add a link with the URL `{unsubscribe_link}`.

Alternatively, Emailify contains a pre-made Loops footer. Click **Footer**, scroll down until you see the Loops logo and click.

<img alt="Emailify footer component" />

You are then free to edit the text and design of the footer (just leave the link URL value as-is).

### Export your email

When you're ready to export, click on **Export HTML** in the top right.

Then in the dropdown select **Loops**, which will generate Loops-friendly MJML.

You can add a Subject and Preview text in this step, too.

<img alt="Exporting an email from Emailify" />

When you're ready, click the **Export for Loops** button and wait for the code to be generated.

To download your files click on the **Download your .zip file** button.

<img alt="Downloading the exported email HTML" />

## Email Love

Email Love is a Figma Plugin that enables you to design and export responsive and accessible HTML emails directly from Figma. Email Love includes an “Export for Loops” option that downloads an MJML file that can easily be imported into Loops.

[Download the Email Love Figma Plugin](https://www.figma.com/community/plugin/1387891288648822744/email-love-html-email-builder)

<img alt="Email Love Figma plugin" />

To run the plugin, click the **Actions** menu in the Figma toolbar, then under the **Plugins & Widgets** tab select **Email Love -> HTML Email Builder**.

### Add components to your email

You can now start designing your email template using Email Love’s pre-built components.

Select the frame you created in the previous section (where you want components to be added). From the plugin pane select one of the component types and then the component you want to add.

<img alt="Adding components" />

### Customize components

You can customize every component you add to your design. Each component features layers and frames that replicate the structure of MJML. For example, selecting **mj-section** in a header component enables you to update the background color and selecting **mj-image** in the header enables you to update the logo.

Go through each component and update the text, images, and styles as you wish.

### Add an unsubscribe link

For campaign and workflow emails you need to include an unsubscribe link in your email.

If you add a **Footer** component Email Love will automatically add a Loops unsubscribe link at the time of export.

Alternatively you can code an unsubscribe link manually by adding a link element with the URL `{unsubscribe_link}`.

```HTML theme={"dark"}
<a href="{unsubscribe_link}">Unsubscribe</a>
```

### Add dynamic content

You may want to add dynamic content (for example, a contact property) into your email. You can do this using dynamic content tags.

For example, adding a first name value can be added like this:

```
Hello {firstName},
```

In workflows you can also add event properties with an `EVENT_PROPERTY:` prefix:

```
Hello {EVENT_PROPERTY:firstName},
```

In transactional emails you can add data variables with a `DATA_VARIABLE:` prefix:

```
Hello {DATA_VARIABLE:firstName},
```

[Read more about dynamic content tags](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax)

### Export for Loops

When your email is finished, click on the frame you want to export. Click **Export** and select "Loops" from the **Export** dropdown. Then click **Export to Loops** to generate a ZIP containing MJML and any included images, which you can then upload into Loops.

## Upload into Loops

Once you have MJML with an unsubscribe link included, you can upload it into Loops.

Click the **Code** styling option in the editor panel. This will reveal the file picker, where you can upload your ZIP file.

<img alt="Uploading a custom email in Loops" />

If you exported from Emailify, open the ZIP you downloaded. Drag and drop the `.zip` file found inside the **\_zips (For upload to Loops.so)** folder. Then click **Upload**.

If you exported from Email Love, upload the generated `.zip` file then click **Upload**.

Your email is now uploaded into Loops and can be sent out!

<img alt="Custom email upload complete confirmation" />


# Using templates
Source: https://loops.so/docs/creating-emails/using-templates

Use Loops templates to draft emails faster: choose a template, customize content, and reuse components.

Loops [templates](https://app.loops.so/templates) help you send professionally designed emails fast. Simply replace the placeholder content with your information, and you’re ready to go.

## Template Variations

Loops offers templates across each of the three [email types](/docs/types-of-emails):

### Campaign templates

[Campaigns](/docs/types-of-emails#campaigns) are the right type of email for a one-off send to your audience or a segment of your audience. These emails are a 1-to-many communication, meaning that the same exact email that you craft can (and probably will) be sent and read by a number of recipients or customers.

<img alt="Announcement Template.png" />

### Workflow templates

[Workflows](/docs/types-of-emails#workflows) are email sequences triggered by an event, a contact being added to your audience, or a contact property update.

<img alt="Workflow template" />

### Transactional templates

[Transactional](/docs/types-of-emails#transactional) emails are automated messages triggered by a specific contact action.

<img alt="Transactional Template.png" />

## How to use templates

1. In your Loops account, hit “Templates” in the left-hand sidebar menu.
2. Browse our selection of `Workflow`, `Campaign` and `Transactional` templates.
3. Select the template that most suits your needs.
4. Edit the template, replacing all placeholder information, links, and images with your content.

<Warning>
  Note: Remember to double-check all links (which can be places other than buttons) and link to the general section, as [dynamic content](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails) can include data variables as well as other things.
</Warning>

5. Once your template is ready to test, send a test email to yourself to see how your design renders in the inbox.


# Add a sending avatar
Source: https://loops.so/docs/deliverability/adding-a-sending-avatar

Set up a sending avatar for your Loops emails using Gmail, Google Workspace, and Outlook to improve open rates and brand recognition in inboxes.

Adding an avatar to your emails can improve open rates and brand recall.

<img alt="example of email sending avatar" />

## Basic setup

1. Log in to the Google Workspace account linked to your Loops sending domain
2. Click the Google logo in the top right corner
3. Click "Add a Profile Picture"
4. Save

<Tip>
  For full coverage, add a [Gravatar](https://en.gravatar.com) icon to the email
  address.
</Tip>

## Advanced setup

### Adding a sending avatar on a secondary domain

To send email from a secondary domain (e.g., mail.loops.so) with a sending avatar:

1. Log in to your Google Admin console with an administrator account
2. Go to "Manage domains" in the dropdown menu
3. Click "Add a domain" and enter the domain name
4. Select "User alias domain" (recommended) or "secondary domain" as appropriate:
   1. **User alias domain** (recommended) – add `notifications.example.com` as an alias domain. This automatically assigns each primary user (for example, `test@example.com`) a corresponding address at `notifications.example.com` without requiring additional seats, and their profile pictures (avatars) will carry over.\
      \
      The User alias domain approach is generally simpler and avoids managing duplicate user accounts.
   2. **Secondary domain** – creates a separate user set, meaning you’ll need to create either a full account or alias for each sender under `notifications.example.com` and upload their profile picture individually.\
      \
      After setup and assigning avatars to each alias in Google, please allow a few minutes for Gmail to update, and your avatars should display consistently.
5. Continue and start verification

Once verified, the secondary domain will use the same sending avatar as the primary domain.

At the end of the day, whoever you are sending from in Loops needs to be a user in G Suite. For example, if you are sending from a `test@example.com`, you need a user with that name in G Suite. It can be an alias or secondary domain, as long as that user exists with a profile pic.

Note that avatars are only visible within the same email client. To display your avatar across all clients, create accounts and upload avatars in each one's settings.

Alternatively, consider using [BIMI (Brand Indicators for Message Identification)](/docs/guides/what-is-bimi), a new email standard for displaying authenticated logos. BIMI requires some setup and legal work but provides an added layer of trust.


# Sending emails to Apple “Hide my email” addresses
Source: https://loops.so/docs/deliverability/apple-hide-my-email

Ensure Loops emails reach Apple Hide My Email addresses by registering your sending domain with Apple.

Apple has recently begun offering their iCloud+ users more and more privacy options when it comes to how they move and appear on the internet.

What does this mean for email? It turns out… quite a bit.

When it comes to email deliverability, [Hide My Email](https://support.apple.com/guide/icloud/set-up-hide-my-email-mm9d9012c9e8/icloud) provides Apple users the opportunity to create unique, random email addresses that forward to their actual email address. This allows them to keep their real email address hidden from online services and email marketers (likely, you).

This can pose challenges for businesses and marketers that hope to track user engagement or maintain an updated and engaged email list.

## Sending emails to customers using Hide My Email

Sending emails to customers that are using Apple’s Hide My Email feature can result in either hard or soft bounces — meaning your email will not be delivered.

If you’re using [Loops](https://loops.so/) as your ESP (email service provider) to send your marketing and transactional emails, an additional step is needed to ensure these members of your audience remain in the loop (pun intended).

It’s simple.

You will need to register your sending domain with Apple through these quick steps.

1. Log in to the Apple Developer Console

2. Click **Services** under the category labeled Certificates, Identifiers & Profiles
   <img alt="Apple Developer Console: Services menu" />

3. Click **Configure**
   <img alt="Apple Developer Console: Configure button" />

4. Click the `+` icon under Email Sources
   <img alt="Apple Developer Console: add Email Source" />

5. Verify SPF and Send Test
   <img alt="Apple Developer Console: verify SPF and send test" />

You'll want to register the SPF domain that we provide for you in-app as well as the usual domain.

For example: `envelope.yourdomain.com` (envelope is the part we are generating for you) and `yourdomain.com`.

That’s it! Goodbye bounces and hello inbox.

## What if I don’t do this?

If you don’t register your sending domain with Apple prior to sending emails it may very likely result in a bounce, meaning that your email will not be delivered to the intended recipient.

If you are using Loops and notice bounces to these email addresses in your email metrics please follow the above steps to register your domain.

Once your domain is registered, reach out to us and we can manually remove the emails from our block list.


# DMARC and DKIM setup
Source: https://loops.so/docs/deliverability/dmarc-dkim-setup

How SPF, DKIM, and DMARC work with Loops, what records you copy into DNS during domain setup, and how to progress your DMARC policy over time.

Email authentication (SPF, DKIM, and DMARC) is what lets Gmail, Outlook, and other inbox providers confirm your email really came from you. Without it, even well-written email lands in spam. For a refresher on the underlying mechanics, see [What is DNS?](/docs/guides/what-is-dns).

Loops provides the records you need. You copy them into your DNS during [domain setup](/docs/sending-domain), and DKIM signing is active from your first send. If DNS is not owned by you, the domain records page in Loops supports [handing DNS setup off to someone else](#handing-dns-setup-to-someone-else).

## What Loops provides vs. what you add

| You add to DNS                                                              | Loops provides                                                         |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| SPF record (at `envelope.<sendingdomain>`, no collision with your root SPF) | DKIM signing on every outgoing email                                   |
| DKIM record                                                                 | A ready-to-copy default DMARC record                                   |
| MX record                                                                   | SPF alignment and envelope-from handling                               |
| DMARC record (with the policy you choose)                                   | Inbox-placement improvements via [Guardian](/docs/creating-emails/guardian) |

The full set of records is visible on your Loops **Settings → Domain** page. See [Setting up your domain](/docs/sending-domain) for the copy-paste-verify flow.

## How DKIM works in Loops

DKIM adds a cryptographic signature to every email you send. Inbox providers use the matching public key, published in DNS, to confirm the message was not tampered with.

During [domain setup](/docs/sending-domain), Loops provides the exact DNS records you need to add so DKIM verification works for your sending domain. You do not generate or manage keys yourself.

<Note>
  Loops' SPF record lives at `envelope.sendingdomain.com` so it does not
  collide with any existing SPF record at your root domain. See the SPF note in
  [Setting up your domain](/docs/sending-domain).
</Note>

## How DMARC works in Loops

DMARC tells inbox providers what to do with email that fails SPF or DKIM. Loops provides a DMARC record during domain setup that you copy into your DNS alongside the other records. You can keep the provided policy or update it over time.

The three policy levels, from least to most strict:

| Policy         | What happens to failing mail | When to use                                |
| -------------- | ---------------------------- | ------------------------------------------ |
| `p=none`       | Delivered, reported only     | Starting out, monitoring                   |
| `p=quarantine` | Sent to spam                 | After 2 to 4 weeks of clean `none` reports |
| `p=reject`     | Blocked entirely             | Once confident, typically 1 to 3 months in |

### Recommended DMARC progression

<Steps>
  <Step title="Start with p=none">
    For the first few weeks, publish `v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com` at `_dmarc.yourdomain.com`.

    This collects aggregate reports without affecting delivery. Use a service like [dmarc.postmarkapp.com](https://dmarc.postmarkapp.com) or similar to parse reports.
  </Step>

  <Step title="Move to p=quarantine">
    Once reports show 100% of legitimate mail is passing DMARC, move to `p=quarantine`. Failing mail goes to spam instead of the inbox.
  </Step>

  <Step title="Move to p=reject">
    After another few weeks of clean reports, move to `p=reject`. At this point, anyone spoofing your domain is fully blocked.
  </Step>
</Steps>

<Tip>
  If you only send email through Loops, you can move through this progression
  faster. The risk is legitimate mail from a forgotten service getting blocked,
  so if Loops is your only sender there is nothing else to miss.
</Tip>

## Setting it up

The full DNS record setup is covered in [Setting up your domain](/docs/sending-domain). In short:

1. Add your sending domain in Loops. A subdomain like `mail.yourcompany.com` is recommended, see [why](/docs/deliverability/sending-from-subdomain). If you send from more than one, see [Sending from multiple domains](/docs/deliverability/sending-from-multiple-domains).
2. Copy the SPF, DKIM, MX, and DMARC records from the domain records page into your DNS provider, or use the [export options](#handing-dns-setup-to-someone-else) to hand that work off.
3. Click **Verify Records** in Loops.

DKIM signing is active from the first send, so your first [campaigns](/docs/types-of-emails#campaigns), [workflows](/docs/workflows), and [transactional emails](/docs/transactional) are all signed.

Once authentication is in place, set up [BIMI](/docs/guides/what-is-bimi) so inbox providers display your verified logo on outgoing mail.

### Handing DNS setup to someone else

If DNS is not owned by you (infra team, IT, or an external DNS provider), the domain records page in Loops has two options that avoid manual copy-paste. See [DNS zone file export](/docs/deliverability/dns-zone-file-export) for the full guide.

<CardGroup>
  <Card title="Zone file export" icon="file-export" href="/deliverability/dns-zone-file-export">
    Download a DNS zone file containing the records Loops provides. Most DNS
    providers can import a zone file directly.
  </Card>

  <Card title="Shareable records page" icon="share-from-square">
    Share a public records URL with whoever is managing DNS. They can read the
    records without access to your Loops account, which also works well for
    coordinating with your own [team members](/docs/account/team-members).
  </Card>
</CardGroup>

Once the records are in place, come back to Loops and click **Verify Records**.

### Migrating an existing domain

If you are switching to Loops from another provider and cannot afford downtime, see [Migrating domains](/docs/deliverability/migrating-domains) for the safe transition path. Your existing SPF/DKIM/DMARC posture matters when deciding whether to send from a subdomain or take over the root.

## Verifying it is working

Loops shows "Records present" on your domain settings page when everything validates. You can also check externally:

* **DKIM**: send yourself an email, view headers, look for `dkim=pass` in `Authentication-Results`
* **SPF**: same header, look for `spf=pass`
* **DMARC**: same header, look for `dmarc=pass`
* **End-to-end**: send a test to `check-auth@verifier.port25.com` or use `mail-tester.com`
* **In Loops**: send your [first email](/docs/sending-first-email) and check `dkim=pass` in the headers of the received copy

For ongoing monitoring, Google Postmaster Tools is worth enabling. See [Gaining deliverability insights](/docs/deliverability/gaining-insights) and, once you are sending at volume, [Understanding email open rates](/docs/deliverability/understanding-email-open-rates).

## Troubleshooting

<AccordionGroup>
  <Accordion title="DKIM fails right after domain setup">
    Most often this is DNS propagation. Records can take up to an hour to
    propagate globally. Wait and re-verify from the domain records page. If
    the records are still missing after an hour, re-copy them (or re-export
    the [zone file](#handing-dns-setup-to-someone-else)) to confirm there is
    no typo.
  </Accordion>

  <Accordion title="DMARC fails but SPF and DKIM pass">
    This is almost always a domain alignment issue. The `From:` header domain
    needs to match either your SPF or DKIM domain. If you are sending from
    `you@yourdomain.com` but Loops signs at `mail.yourdomain.com`, you need
    relaxed alignment (`aspf=r; adkim=r`) in your DMARC record, which is the
    default in the record Loops provides.
  </Accordion>

  <Accordion title="Subdomain setup but DMARC is at root">
    A DMARC record at the root domain (`_dmarc.yourdomain.com`) applies to all
    subdomains by default. You do not need a separate DMARC record at
    `_dmarc.mail.yourdomain.com` unless you want a different policy there.
  </Accordion>

  <Accordion title="Multiple sending domains and overlapping DMARC">
    If you send from more than one domain, each needs its own records. See
    [Sending from multiple domains](/docs/deliverability/sending-from-multiple-domains).
    A single root-level DMARC still applies to all subdomains unless you
    override it.
  </Accordion>

  <Accordion title="Still seeing deliverability issues">
    Authentication is necessary but not sufficient. Check your [sender
    reputation](/docs/deliverability/sending-reputation), [list
    hygiene](/docs/deliverability/maintaining-a-clean-list), and [inbox placement
    fundamentals](/docs/deliverability/improving-inbox-placement). For ongoing
    signal, see [Gaining deliverability
    insights](/docs/deliverability/gaining-insights). If you are sending at high
    volume, [Sending to a large
    audience](/docs/deliverability/sending-to-large-audience) is worth reviewing.
  </Accordion>
</AccordionGroup>

## Read more

<CardGroup>
  <Card title="Setting up your domain" icon="globe" href="/sending-domain" />

  <Card title="Sending from a subdomain" icon="sitemap" href="/deliverability/sending-from-subdomain" />

  <Card title="What is BIMI?" icon="id-badge" href="/guides/what-is-bimi" />

  <Card title="Improving inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />
</CardGroup>


# DNS zone file export
Source: https://loops.so/docs/deliverability/dns-zone-file-export

Download DNS records as a zone file or share a public records page.

From the domain records page, you can download a DNS zone file or share a public link to the records. This saves you from copying SPF, DKIM, MX, and DMARC records one by one.

## When to use a zone file

If your DNS provider supports zone file imports, upload the file instead of entering each record manually.

## When to share the records page

Share the public records page when:

* You want someone to review or troubleshoot the records without signing in to Loops.
* A developer, agency, or IT team is handling DNS for you.

## How to export a zone file or share the records page

1. Go to **Settings → Domain** in Loops, or [open the domain records page directly](https://app.loops.so/sending-domain).
2. Download and import the zone file, or copy the public records URL and send it to whoever manages your DNS.
3. Once the records are in place, return to Loops and click **Verify Records**.

## Importing a zone file in common providers

* [Cloudflare: Import and export DNS records](https://developers.cloudflare.com/dns/manage-dns-records/how-to/import-and-export/).
* [Route 53: Creating records by importing a zone file](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-creating-import.html).
* [GoDaddy: Import my domain's zone file records](https://www.godaddy.com/help/import-my-domains-zone-file-records-4167).
* [DNSimple: Import Zone File](https://support.dnsimple.com/articles/import-records-zone-file/).

For other providers, search their documentation for "zone file import" or "BIND zone file."

## After you import

DNS changes can take up to an hour to propagate. Once they do, click **Verify Records** in Loops to confirm the configuration.


# Insights from Google Postmaster
Source: https://loops.so/docs/deliverability/gaining-insights

Gmail doesn't report spam reports to Loops, so setting up Google Postmaster is key to getting an idea of your deliverability performance in Gmail.

Google Postmaster is a free tool that provides users with in-depth data and analytics to help them understand how their emails are performing in Gmail.

This tool provides senders with data on various metrics such as spam rate, domain reputation, email authentication, and delivery errors. By analyzing these metrics, you can identify the issues that may be causing your emails to be marked as spam or not delivered.

You can sign up for Google Postmaster here: [https://gmail.com/postmaster/](https://gmail.com/postmaster/)


# Improve your inbox placement
Source: https://loops.so/docs/deliverability/improving-inbox-placement

If your emails are not landing in the inbox folder you expect, here are some tips to improve your inbox placement.

Your email deliverability is an ever-evolving metric that can be influenced by a variety of factors.

It's composed of, but not limited to, a combination of the following:

* sending IP
* your domain
* your content
* email clients' history of those
* the content of your current message
* blocklists
* sending frequency
* TLD (Top Level Domain)

Follow these steps to resolve any issues you may be experiencing with your email deliverability. When these steps are followed, we typically see a significant improvement in inbox placement.

<Tip>
  Want help auditing deliverability and lifecycle setup? Use the
  [Email best practices skill](/docs/skills#email-best-practices-skill) so your coding
  agent can review sender setup, list hygiene, consent handling, and content
  risk patterns. Install Loops skills with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/skills | sh
  ```
</Tip>

## 1. Confirm Domain Record Setup

Ensure your domain records are properly set up is a huge piece of the puzzle. If you already use Loops, you can just visit the [Domain Records](https://app.loops.so/sending-domain) page to see if your records are set up correctly.

## 2. Recent Domain Record Changes

Following on the previous point, if you've added or updated domain records within the last 24 hours, wait for changes to propagate. This is especially important if you've just set up your domain for email sending.

## 3. Don't Send Cold Emails

Avoid sending cold emails from your primary domain or a subdomain linked to it. Use a separate domain dedicated to sales efforts. Any cold emails at all can have a negative impact on your deliverabilty. Talk to your sales team. Please do not send cold emails with Loops.

## 4. Historical Use of Primary Domain for Cold Email

If you have previously used your primary domain for cold emailing, you may need a new domain for sending emails, especially if your domain reputation has had issues in the past. It's not impossible to fix your domain reputation, but it can be a long process. If you're a startup, a fresh domain might be the right choice.

## 5. Send a Welcome Email

[Implement a welcome email with a workflow](/docs/guides/onboarding-emails) to welcome new users as soon as they join. When a user signs up, they expect a welcome email, so intent is clear and engagement is high. Keeping a steady cadence of emails to all new users will help balance out larger email sends to your entire user base. This is especially important if you're a new sender.

## 6. Content of Emails

Review the content of your emails and refrain from sending emails about crypto, giveaways, financial products or a regulated industry which are typically flagged by email clients. If you need to send these types of emails, review the content and ensure it's compliant with email client policies.

## 7. Use of Popular Top-Level Domains (TLDs)

Consider sending email from a domain with a popular top-level domain (TLD) such as .com or .org. These TLDs are less likely to be flagged as spam by email clients. A few less common TLD's like .io, .ai and .so seem to be fine as well. However brand new TLD's as well as specifically .xyz and .info are often flagged as spam.

## 8. Send Content Your Users Want

Ensure your email content is desirable and relevant to your audience. Focus on valuable updates such as product information rather than unsolicited giveaways or promotions. Would you want to read the email if it came from a service you use? If not, consider changing the content.

## 9. User Consent

Only send emails to users who have explicitly opted in to receive communication from you. Do not email without explicit consent.

## 10. Google Postmaster Tools

Set up [Google Postmaster Tools](/docs/deliverability/gaining-insights) to gain insights into your email performance and identify potential issues.

## 11. Incorrectly Flagged

Occasionally, when setting up emails from a new domain that matches your workspace domain, you may run into issues where Gmail or another email client incorrectly flags the message as spam or phishing.

You can usually confirm this is the case by sending the same email to a different address—for example, a personal account or one on another domain. If the email is delivered normally there, the issue is with how the client is classifying messages from the new domain.

If this happens, simply mark the message as "Not spam" or "Not phishing" in Gmail. The issue should resolve itself shortly.

This behavior is not caused by Loops, it's a built-in protection mechanism that email clients use when learning to trust a new domain.

## 12. Apple iCloud Private Relay

[iCloud Private Relay](https://support.apple.com/102602) is a privacy feature for iCloud+ subscribers that masks users' IP addresses and encrypts their web traffic. If you're sending to Apple users, ensure your domain is [properly registered with Apple](/docs/deliverability/apple-hide-my-email) to avoid deliverability issues with Apple users.

## Additional Checks: Blocklists and IP Reputation

If you've checked all the above and are still having issues, you may want to check if your IP address is on a blocklist. You can use [MXToolbox](https://mxtoolbox.com/blacklists.aspx) to check if your IP address is on a blocklist. Please note that blocklists change frequently and usually resolve themselves within a few days.

## Read more

<CardGroup>
  <Card title="Understand email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />

  <Card title="Build your sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Maintaining a clean list
Source: https://loops.so/docs/deliverability/maintaining-a-clean-list

A clean list is a happy list. Here’s how to keep your list clean and your emails deliverable.

One of the pillars of good email deliverability is **list hygiene**. Keeping your list clean and up-to-date is the first step to ensure that your emails are being delivered to the right people.

Most of the emails on your list will likely be legitimate emails, but if you have an in-demand service, users may create temporary email addresses or attempt other methods to gain access to your service and evade limitations you set.

Temporary or fake email addresses are often used once and then abandoned, and can cause your emails to bounce. If you send to too many of these addresses, your deliverability will suffer as the invalid ones will bounce and the valid (but fake) ones will mark your emails as spam.

<Tip>
  Want help auditing deliverability and lifecycle setup? Use the
  [Email best practices skill](/docs/skills#email-best-practices-skill) so your coding
  agent can review sender setup, list hygiene, consent handling, and content
  risk patterns. Install Loops skills with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/skills | sh
  ```
</Tip>

A few things you can do to keep your list clean:

### 1. Protect the inputs

The easiest way to limit pain is to prevent it from happening in the first place. Start with cutting off the source of the problem by protecting the inputs (your signup forms).

* Use a captcha
* Enable Cloudflare’s bot protection on your site
* Use form validation to ensure that the email address is valid
* Block disposable email addresses
* Use a double opt-in

### 2. Use a service that handles deliverability best practices for you

* Loops automatically handles bounces, unsubscribes, and spam complaints for you
* Just use Loops

### 3. Look for warning signs

* If your open rates are low, it’s likely that you’re sending to a lot of invalid addresses or sending content your subscribers don’t want
* If your open rates are high but then drop off, it could be a sign that your emails are being marked as spam or that poor list hygiene is causing your emails to bounce


# Migrating domains
Source: https://loops.so/docs/deliverability/migrating-domains

Switching to a new sending domain.

You may need to switch sending domains if you rebrand your company, want to send from a new domain for the first time, or are migrating to Loops from another provider.

Keep the following in mind:

1. **DNS records matter**

   If DNS records for your domain are removed, email sending will stop, just as it would with any other provider.
2. **Avoid migrating if your reputation is good**

   If your sender reputation is already healthy, we generally recommend **not migrating**. The downtime and adjustment period can cause more harm than benefit.
3. **If you must migrate, choose the right timing**
   * For **transactional email**, migrate during your app’s off-peak hours.
   * For **marketing or product updates**, migrate between major sends.
4. **Get help if needed**

   If you have concerns, [reach out to our support team](https://app.loops.so/settings?page=support) ahead of time. We’re happy to clarify best practices and help you plan a smooth migration.


# Deliverability optimization
Source: https://loops.so/docs/deliverability/optimization

How we monitor sends to increase the deliverability of your emails.

During the email sending process, you may see a notification indicating that your email is being monitored for deliverability. This is a proactive measure to ensure that your emails are being delivered to the inbox. A combination of factors are taken into account that may affect deliverability, such as:

* Queuing of emails for delivery
* Email content analysis
* Email sending patterns
* Email sending volume
* Client reputation and email sending history indicators
* Much more!

We have guardrails in place to protect our user's email deliverability (along with yours) and if you see this message, you're seeing one of those guardrails in action.

Emails will be monitored on send to ensure they're hitting the inbox. If you have any questions, [contact support](https://app.loops.so/settings?page=support).

<Tip>
  Want help auditing deliverability and lifecycle setup? Use the
  [Email best practices skill](/docs/skills#email-best-practices-skill) so your coding
  agent can review sender setup, list hygiene, consent handling, and content
  risk patterns. Install Loops skills with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/skills | sh
  ```
</Tip>

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understand email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Build your sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Reducing product email bounce rate
Source: https://loops.so/docs/deliverability/reducing-bounce-rate

How to reduce product email bounce rate: hard vs soft bounces, what Loops handles automatically, and the actions you can take when your rate is too high.

A high bounce rate degrades your [sender reputation](/docs/deliverability/sending-reputation) quickly. Mailbox providers treat repeated bounces as a signal that you are sending to lists you should not have. This guide covers what Loops handles automatically and the actions you can take when your rate is too high.

## Hard vs soft bounces

| Type            | What it means                                                                     | What Loops does                                                                          |
| --------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Hard bounce** | Permanent failure. Address does not exist, domain does not exist, mailbox closed. | Adds the contact to [suppression](/docs/contacts/suppression) so Loops stops sending to them. |
| **Soft bounce** | Temporary failure. Mailbox full, server down, message too large, graylisted.      | Retries transient failures and tracks soft bounces separately from hard bounces.         |
| **Complaint**   | Recipient marked it as spam.                                                      | Same as hard bounce. Contact goes into [suppression](/docs/contacts/suppression).             |

## What Loops handles automatically

* Adds hard-bouncing and complaining addresses to [suppression](/docs/contacts/suppression), per [contact suppression](/docs/contacts/suppression)
* Retries transient/soft failures
* Rate-limits and batches campaign sends, see [Sending to a large audience](/docs/deliverability/sending-to-large-audience)
* Applies [double opt-in](/docs/contacts/double-opt-in) to form submissions when enabled, so pending contacts are excluded from marketing sends until they confirm

## Driving your bounce rate down

### Protect your signup forms

Most bounces trace back to the address getting into your list. Fix it at the source:

* Add a captcha to every public signup form
* Enable bot protection at the edge (Cloudflare or similar) to absorb scripted fake-signup traffic
* Validate email syntax client-side and server-side before submitting to Loops
* Turn on [double opt-in](/docs/contacts/double-opt-in) for form submissions so unconfirmed contacts stay out of marketing sends
* Block disposable providers using the [email blocklist](#use-the-email-blocklist) (below)

If you are using [Loops forms](/docs/forms/simple-form), rate limiting and basic abuse protection are already in place. For deeper background on list hygiene see [Maintaining a clean list](/docs/deliverability/maintaining-a-clean-list).

### Verify your sending domain

DNS misconfiguration is a hidden contributor to bounce rate. Even soft bounces climb when DKIM or SPF fail, because mailbox providers hold your mail longer or drop it.

* Complete [sending domain setup](/docs/sending-domain)
* Send from a subdomain, see [why](/docs/deliverability/sending-from-subdomain)
* Review [Improving inbox placement](/docs/deliverability/improving-inbox-placement) for the broader checklist

### Stop sending to disengaged users

If a contact has not opened anything in 60+ days, they are a bounce risk even if their address is technically valid. Stale addresses get abandoned and mailboxes fill up.

Campaign only to active users. If you sync product activity into Loops, create a custom Date contact property such as `lastActive` and update it when users log in or take a meaningful action. Then save a segment that filters `lastActive` to the last 30 days on the [Audience page](https://app.loops.so/audience), see [filters and segments](/docs/contacts/filters-segments). For setup details, see [sending a campaign to active users](/docs/guides/onboarding-emails#sending-a-campaign-to-active-users-in-loops). Keep the rest of the Audience intact, but only actively mail people who still engage.

<Warning>
  We do not recommend running re-engagement campaigns aimed at cold contacts. Sending more email to disengaged users tends to increase complaints and hurt deliverability further. It is almost always better to let them sit and focus on the engaged segment.
</Warning>

### Use the email blocklist

The [email blocklist](/docs/contacts/email-blocklist) (under **Settings → Account**) blocks patterns before a contact is ever created, across every signup path: manual add, CSV upload, forms, the API, and integrations.

Three pattern styles are supported:

* Full address to block one specific user: `blockthisuser@domain.com`
* Wildcard domain to block a disposable-email provider: `*@mailinator.com`
* Wildcard TLD to block a class of addresses: `*@*.edu`

Common patterns worth adding early:

* Known disposable-email domains (tempmail, mailinator, guerrillamail, etc.)
* Role-based addresses that almost always bounce or route to shared inboxes you do not want (`admin@`, `postmaster@`, `abuse@`, `noreply@`)
* Specific domains that have blocked you or produced a cluster of complaints

### Warm up carefully on new domains

A brand-new sending domain with zero history will bounce more. If you are starting from scratch or migrating, follow the reputation-building flow in [Your first onboarding emails](/docs/guides/onboarding-emails): start with transactional and welcome sends to engaged users, then grow volume gradually while monitoring open rates.

For migrations specifically, see [Migrating domains](/docs/deliverability/migrating-domains). For broader context, our other deliverability guides cover the adjacent practices that keep bounces low:

* [Building your sender reputation](/docs/deliverability/sending-reputation)
* [Improving inbox placement](/docs/deliverability/improving-inbox-placement)
* [Maintaining a clean list](/docs/deliverability/maintaining-a-clean-list)
* [Sending to a large audience](/docs/deliverability/sending-to-large-audience)

### Don't use third-party list-cleaning tools

We do not recommend running your Audience through external email-verification or list-cleaning services. In practice these tools:

* Produce a lot of false positives (flagging valid mailboxes as invalid), which shrinks your reachable audience for no benefit
* Can themselves generate probe traffic that hurts your sender reputation
* Do not fix the upstream problem, which is usually signup-form hygiene or sending to disengaged contacts

Loops already suppresses hard bounces and complaints automatically. The right investment is cleaner signup inputs and tighter audience segments, not a verification pass after the fact.

### Don't use domain warmup tools

Third-party warmup services that send automated back-and-forth emails between seeded inboxes do not build real sender reputation. Inbox providers weight engagement from actual recipients, and synthetic warmup traffic is increasingly easy to detect and discount.

If you are sending to real, engaged users, a warmup tool should not be necessary. Follow the natural warmup path in [Your first onboarding emails](/docs/guides/onboarding-emails): start with transactional and welcome sends, then grow volume gradually as engagement metrics stay healthy. That produces a reputation that actually holds up.

## Diagnosing high bounce rates

When a send has unexpectedly high bounces, the specific failure reasons tell you where to focus:

* **Domain not found**: typo-filled addresses, often from form spam. Fix with [signup-form protection](/docs/deliverability/maintaining-a-clean-list).
* **Mailbox does not exist**: stale addresses, often from old imported lists. Segment out disengaged contacts before sending.
* **Blocked**: your [sender reputation](/docs/deliverability/sending-reputation) is low at that provider. Focus on engaged-user sends and review [inbox placement fundamentals](/docs/deliverability/improving-inbox-placement).

## Bounce rate guidance

General industry benchmarks, to interpret alongside your own trend:

| Bounce rate | Status                                     |
| ----------- | ------------------------------------------ |
| Under 2%    | Healthy                                    |
| 2% to 5%    | Worth investigating                        |
| Over 5%     | Deliverability at risk, pause and diagnose |

If you are consistently over 5% on campaigns, the fix is almost always a segmentation problem, not a content problem. Narrow your audience to engaged users first using [filters and segments](/docs/contacts/filters-segments).

## Read more

<CardGroup>
  <Card title="Maintaining a clean list" icon="broom" href="/deliverability/maintaining-a-clean-list" />

  <Card title="Improving inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Sender reputation" icon="shield" href="/deliverability/sending-reputation" />

  <Card title="Contact suppression" icon="circle-xmark" href="/contacts/suppression" />
</CardGroup>


# Sending from multiple domains
Source: https://loops.so/docs/deliverability/sending-from-multiple-domains

The pros and cons of sending from multiple domains and how to do so in Loops.

Much of the information online about email best practices is outdated. Email is a very old industry, and while filtering technology has evolved, much of the guidance still referenced has not.

In our experience, sending from a **single domain** offers far more benefits than using multiple domains. Here’s why:

1. **Consistent sender history**

   The hardest part of email sending is building and maintaining a positive sender reputation. This requires steady, high-quality sending volume over time. Splitting traffic across multiple domains fragments this history, making it much harder to maintain consistency.
2. **Avoiding weak or “cold” domains**

   When a domain is only used occasionally, say, just for product updates, it lacks a sending history. As a result, when you suddenly send a large campaign from that domain, it is far more likely to land in spam. Using a single domain avoids this problem as sending history is shared and consistent.
3. **Modern filtering methods**

   In the past, spam filtering focused heavily on the sending domain or IP address. This made splitting domains a defensive strategy in case one was blocklisted.

   Today (2025 and beyond), most inbox filtering is powered by large language models (LLMs). These systems evaluate content quality and sender reputation over time, not just the domain name. That means if you consistently send high-quality content from one well-established domain, your deliverability will be far stronger than spreading efforts across many domains.

If you do want to separate different types of communication, such as product updates vs. newsletters, we recommend using our [**Mailing List**](/docs/contacts/mailing-lists) feature. It allows subscribers to manage their preferences and opt into specific types of emails, all from a single domain. This avoids domain fragmentation while still giving your users choice.

If you truly need to send from multiple domains, you can create a separate team in Loops and switch between them easily. Just note that settings, audience records, and other data are not shared across teams.


# Subdomains vs root domains
Source: https://loops.so/docs/deliverability/sending-from-subdomain

Most email services (including Loops) prefer sending emails from subdomains (hey.company.com) over root domains (company.com).

Subdomains can often be easier to configure and can sometimes be more effective at preventing email from being marked as spam.

It is important to remember that emails sent from a subdomain will also appear in the “from” field of your emails, so make sure to use an address that is recognizable to your audience. We recommend sending from a subdomain like:

* hey.company.com
* mail.company.com
* updates.company.com

Whichever you choose, you can always update it later in your [Domain Settings](https://app.loops.so/settings?page=domain).


# Building your sender reputation
Source: https://loops.so/docs/deliverability/sending-reputation

Understanding how to build your sender reputation.

This is a high-level guide, mostly based on our experience as the makers of Loops. We're going to touch on some industry terms and best practices, but ultimately we're focusing on how successful SaaS companies have built their sender reputation using Loops.

You might be reading this guide because you're trying to understand if migrating to Loops and changing your domain is a good idea, if you need to warm up your IP or domain, or if you're just curious about how it all works.

Your sender reputation, as defined by us, is a calculated value based on your domain and your sending IP. This calculation also considers factors like engagement rates, sending volume, and email content quality. We reference this throughout our documentation, and in our support chats, so it's important to understand how it works.

<Tip>
  Want help auditing deliverability and lifecycle setup? Use the
  [Email best practices skill](/docs/skills#email-best-practices-skill) so your coding
  agent can review sender setup, list hygiene, consent handling, and content
  risk patterns. Install Loops skills with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/skills | sh
  ```
</Tip>

Email clients, such as Gmail, Outlook, and others, also judge sender reputation using largely opaque algorithms. Each factor, such as engagement rates and sending consistency, contributes to whether your email will be successfully delivered.

## Sending Domain

Your domain is the url (e.g., example.com) that you use to send emails. A common best practice is to send from a dedicated subdomain (for example, `mail.example.com`) so your sending domain stays separate from your primary domain.

A key rule in email sending is that a domain with a strong sending history—consistent volume and high engagement—helps your sender reputation. Conversely, a domain with a weak history can harm it. This can be frustrating when trying to improve your reputation, but if your current sender reputation is good, there’s no need to change your domain.

Changing domains or IP addresses can lead to a reset of your sender reputation, requiring you to rebuild your score from scratch.

This process can be risky and time-consuming, often without significant benefit if your current sender reputation is strong. It’s generally advisable to stick with your existing domain and IP unless you're facing significant deliverability issues.

Other factors can also play a role:

* The volume of emails you send
* The type of content you send
* The users receiving your emails

Beyond that, factors like your subdomain, your domain's age, and even the TLD of the domain matter because they contribute to the perceived trustworthiness of your emails.

## Sending IP

Your IP reputation is largely based on its sending history. If you send from an IP that has a strong sending history, it will help your sender reputation. If you send from an IP that has a weak sending history, it will hurt your sender reputation. Additionally, factors like email frequency, volume, content quality, and recipient engagement play important roles.

If you change either your domain or IP, your sender reputation will need to be rebuilt, which requires following best practices and potentially reducing email volume until a strong reputation is established.

Keep in mind that your sender reputation is made up of both your domain and IP.

## Wrapping Up

Follow the best practices listed in our docs, send relevant content to engaged users, and maintain a consistent sending volume to keep your reputation strong.

If you're starting fresh, consider using a subdomain for sending emails and keep your sending volume conservative initially.

For any questions, just ping [chris@loops.so](mailto:chris@loops.so).

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understand email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />
</CardGroup>


# Sending to large audiences
Source: https://loops.so/docs/deliverability/sending-to-large-audience

Tips for sending emails to a large group of new contacts.

Most users in this scenario face two key challenges:

* You have a large audience that you haven't contacted before or rarely have in the past.
* Your domain has little to no sending history.

As a result:

* Users are not expecting emails from you, as they haven't received any in the past.
* Email clients (Gmail, Outlook, etc.) don't recognize you, so your sender reputation is nonexistent.

It’s **really important** to handle this well, so follow these steps or contact [chris@loops.so](mailto:chris@loops.so) with any questions.

<Tip>
  Want help auditing deliverability and lifecycle setup? Use the
  [Email best practices skill](/docs/skills#email-best-practices-skill) so your coding
  agent can review sender setup, list hygiene, consent handling, and content
  risk patterns. Install Loops skills with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/skills | sh
  ```
</Tip>

### "Warming Up" Your List

We want to build your sender reputation **and** start emailing your best users at the same time. Thankfully, this is easier than it sounds.

The easiest way to do this is *not* by contacting your existing list but by contacting users that are just joining your platform for the first time. Why? Because in our experience, as individuals and as the makers of Loops, the most likely time a user will engage with your emails is when they first join your platform.

### Setting Up an Onboarding Workflow

So how do we start emailing these new users?

Start by [setting up a workflow](/docs/workflows#building-a-workflow) that greets users as soon as they join your platform.

These users are the most active and are most likely to respond favorably, positively impacting your business and your sender reputation.

You should let this workflow run for a few days while your sender reputation is built up and you have a chance to understand how your users react to the type of emails you're sending.

Ideally, open rates will now be >40% at this point for the first email in your workflow.

### Engaging Your Most Active Users

After your onboarding workflow has run for a few days, you can begin to backfill your audience in Loops by importing a CSV of your existing users or by using our API.

While you can (and probably should) import all existing users at this point, we recommend contacting users that signed up over the last seven days prior to the start of the workflow we set up.

Depending on audience size, you could also import users from up to three months ago, but our goal here is to make sure we are contacting users that still engage with your platform.

Alternatively, instead of importing users by the creation date of their account, you can do it by their last login date if you have that information stored.

If you don't have the information stored, you could start now and import users in a week or two after you have an idea of who your most engaged users are.

### General Recommendation for Importing Contacts

A general recommendation would be to import any contacts who have logged in within the last 90 days and then also set up a workflow to welcome all new users going forward. Then you get transactional sending to any contacts in your audience free of charge as well.

### Expanding to Older Users

After your most engaged users have been contacted and your workflow has continued to send, you should have a good idea of how your users are responding to emails that you send them.

You can then begin to expand your audience and import some older users. We recommend sending in batches to limit any potential issues with the domain reputation.

If you notice your open rates dipping below average at any point, it’s a good indication that you’ve reached as far back into your audience as you’ll want to go. At that point, it’s a good idea to only contact users that were previously on your list if they reengage on your platform with a login.

### Wrapping Up

By following these steps, you should be able to successfully send emails to your user base without damaging your domain reputation. Remember to always monitor your open rates and engagement levels and adjust your email strategy accordingly.

For any questions, just ping [chris@loops.so](mailto:chris@loops.so).


# Sending transactional emails to group inboxes
Source: https://loops.so/docs/deliverability/transactional-group-inboxes

Configure SPF, DKIM, and sender settings so transactional emails sent through Loops are reliably delivered to Google Groups and other shared inboxes.

When sending [transactional emails](/docs/transactional) to a group address, you may occasionally encounter issues with email delivery due to settings within your Google Workspace Admin. This guide explains how to handle these situations and prevent them in the future.

Group inboxes are intended as a catch-all email address that receives email and relays it on to many recipients. Transactional email however is intended as a one-to-one communication method and is generally not appropriate for group inboxes. As a result, group inboxes may have additional filtering in place, often by default, which impedes your ability to receive emails like 2FA, click to login and others.

These emails include transactional emails sent to your customers and users from Loops and other services, but also the emails sent by us when you log in.

Here are some steps to help you troubleshoot and prevent these issues.

## Google Workspace

### Checking for blocked emails

If you're not receiving expected emails to a group inbox, check these locations:

#### Google Groups Pending Conversations

* Go to [groups.google.com](https://groups.google.com)
* Go to **Pending** conversations in the left sidebar.
  <img alt="Pending conversations" />
* Click the checkmark icon on the right to approve individual messages.
  <img alt="Approve message" />

#### Email logs

* Check the email logs at [admin.google.com/ac/emaillogsearch](https://admin.google.com/ac/emaillogsearch)
* Search for specific emails to see their status. Look for emails labeled with "Sent for moderation".

#### Google Admin Moderation queue

* Go to [admin.google.com/moderation](https://admin.google.com/moderation) to view all quarantined email messages.
* Check the queue for emails to group inboxes.

### Preventing future issues

To prevent important emails from being blocked, you can change a setting in Google Groups.

Go to the Group settings in Google Workspace. In the **Posting policies** section, change the **Spam message handling** selection to "Post suspicious messages to the group".

<img alt="Spam setting" />

## Other troubleshooting steps

If you're still having issues:

1. Verify the email is being sent successfully. For example, in Loops you can check the **Metrics** page of your transactional email to see if the email was sent, delivered and bounced.
2. Check if the email is being received by other group members.
3. Contact your workspace administrator for assistance with group settings.

## Read more

<CardGroup>
  <Card title="About transactional email" icon="envelope" href="/transactional" />

  <Card title="Transactional guide" icon="file-code" href="/transactional" />

  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />
</CardGroup>


# Email open rates
Source: https://loops.so/docs/deliverability/understanding-email-open-rates

Understand how open rates can be affected by clients and networks.

Have you ever sent out an email campaign and noticed that some of the recipients showed as "opened" nearly instantly upon sending, while others took a while or didn't show at all? This discrepancy can be confusing and make it difficult to interpret your email open rates accurately.

If your contacts are using Apple Mail or are behind corporate firewalls, it's likely that your open rates are being affected by email privacy measures. Here's what you need to know.

## Apple Mail

Apple Mail Privacy Protection: Apple Mail is a popular email client that is used by millions of people worldwide. In an effort to protect user privacy, Apple introduced Mail Privacy Protection with the release of iOS 15, iPadOS 15, and macOS Monterey. This feature blocks email tracking pixels, obscuring email open rates, and making it difficult to know exactly when an email has been opened.

If your recipient is using Apple Mail to access their Gmail or other email provider's inbox, then their email will be proxied through Apple's servers, which will affect your open rates.

## Corporate firewalls

Many companies use firewalls to protect their network and prevent unauthorized access to their data.

These firewalls can also affect email open rates by blocking or caching tracking pixels, which are used to track when an email is opened. If your recipient is behind a corporate firewall, then their email client may not be able to download the tracking pixel, which will affect your open rates. If they cache the pixel, it may show as opened immediately.

So, what can you do to get a better understanding of your email open rates? Here are a few tips:

1. **Use other metrics:** While open rates can be a useful metric for measuring the success of an email campaign, they're not the only metric you should be using. Click-through rates, conversion rates, and revenue generated are all important metrics that can give you a better understanding of how your email campaign is performing.
2. **Use a different tracking method:** If you want to track email opens more accurately, you can use a different tracking method that doesn't rely on tracking pixels. For example, you could use a unique link in your email that redirects to your website. By tracking the number of clicks on that link, you can get a better understanding of how many people opened your email.

Open rates are just one metric among many that you should be using to measure the success of your email campaign. By using other metrics, segmenting your audience, and using different tracking methods, you can get a more accurate picture of how your email campaign is performing.

If you want to measure whether a campaign drove meaningful engagement or a
business outcome, attach a [goal](/docs/goals/overview) to the campaign and read the
[goal metrics](/docs/goals/metrics). Goals track conversions against the target you
define, which is usually a better success signal than opens or clicks alone.

## Segment by opens and clicks

For campaigns and workflow emails, you can use contact activity filters to
build segments from sends, opens, and clicks. For example, you can create a
segment of contacts who opened a campaign, clicked a workflow email, or were
not sent a selected campaign.

Use [filters and segments](/docs/contacts/filters-segments#activity-filters) to
save these groups for future sends, or [export contacts](/docs/contacts/export-contacts)
to download a filtered CSV.

<Note>
  Transactional emails do not track opens or link clicks. If you need a
  reliable conversion signal after a transactional email, track the product
  action in your application.
</Note>

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understand email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />

  <Card title="Build your sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Shortened YouTube links warning
Source: https://loops.so/docs/deliverability/youtube-links-warning

Information about shortened YouTube links being flagged in Gmail.

We are currently flagging shortened YouTube links (youtu.be) in emails because Gmail is incorrectly identifying them as phishing links.

## What's happening

Gmail's spam filters are flagging emails containing shortened YouTube links as potential phishing attempts. To protect your deliverability, our [Guardian](/docs/creating-emails/guardian) feature will warn you if your email contains these links.

## Recommendation

Use full YouTube URLs instead of shortened links:

* **Avoid:** `youtu.be/abc123`
* **Use:** `youtube.com/watch?v=abc123`

## Status

We are actively monitoring this situation and will retire this warning once Gmail resolves the issue.


# Events: trigger automated emails with Loops
Source: https://loops.so/docs/events

Events let you trigger workflows and emails when something happens in an external platform.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

## About events

Events represent a payload of data that can be used to trigger emails via [workflows](/docs/workflows) and can contain [personalized data](/docs/events/properties) for each email.

Events can be sent to Loops with [the API](/docs/api-reference/send-event) or via [an integration](/docs/integrations).

There are two types of events:

* **Custom events** are events that you create yourself.
* **Platform events** are events that are triggered by [incoming webhook](/docs/integrations/incoming-webhooks) integrations from Stripe, Clerk, and Supabase. You cannot edit or create platform events.

## Creating events

Events can be created automatically or manually in several ways. As well as specifying a name, you can also create [event properties](/docs/events/properties) with each of these methods.

<Tip>
  Custom event names can not contain colon characters (`:`).
</Tip>

### Automatically by sending an event

You can create an event by sending an event with a new event name.

For example, `creditWarning` here is a new event. This request will create a new event in your account.

```json theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "creditWarning",
  "eventProperties": {
    "creditsPurchased": 1000,
    "creditsUsed": 903,
    "accountName": "Loops"
  }
}
```

### Manually from the Events page

Navigate to the [Events page](https://app.loops.so/settings?page=events) to create an event manually.

<img alt="Add an event from Settings" />

### Manually from an Event trigger node

When [building a workflow](/docs/workflows), use the **Event received** trigger node to create an event (see below).

## Specifying an event for a workflow

You can use events to trigger email sending within workflows.

When creating or editing a workflow, select **Event received** in the **Trigger Type** dropdown in the inspector panel on the right-hand side.

Type in the name of a new event or start typing the name of an existing event, then select from the dropdown.

<img alt="Add an event from the workflow builder" />

## Sending events

Now you have events set up in your Loops account, you can start sending custom events using the API or integrations.

### Sending events with the API

Using the API, send events to the [Send event endpoint](/docs/api-reference/send-event):

```
POST https://app.loops.so/api/v1/events/send
```

All that's required is an `email` or `userId` to identify the contact, plus the `eventName`.

```json theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "planUpgrade"
}
```

The `eventName` can either be an existing custom event in your account or a new event name.

If a contact is not found with the provided `email` or `userId`, a new contact will be created.

You can include [contact properties](/docs/contacts/properties) in the request, which will be saved onto the contact. These contact properties can be used in emails to your contacts or for [filtering and segmenting your audience](/docs/contacts/filters-segments).

```json theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "planUpgrade",
  "firstName": "Sam",
  "favoriteColor": "red"
}
```

To provide your emails with event-specific data, include event properties in the request. [Read more about event properties](/docs/events/properties)

```json theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "planUpgrade",
  "firstName": "Sam",
  "favoriteColor": "red",
  "eventProperties": {
    "newPlan": "Pro",
    "oldPlan": "Basic",
    "planPrice": 29,
    "isLifetime": false,
    "updateDate": "2024-03-07T10:09:23Z"
  }
}
```

### Sending events via an integration

Many of [our integrations](/docs/integrations#send-email) also support sending events, giving you options to send events without building with our API.

## Viewing and editing events

### Event patterns

In your Loops Settings the [Events page](https://app.loops.so/settings?page=events) shows the list of your different events, including how many times they've been sent. Counts are updated hourly.

<img alt="Events page" />

To edit event properties, click on an event type. You can also edit events by selecting an **Event received** trigger in the workflow builder, then clicking **Edit event properties** in the inspector panel.

<img alt="Editing an event" />

Events cannot be deleted.

### Event logs

Below the list of events, you can see the event logs for your account. This includes events sent to us [via the API](/docs/api-reference/send-event) and any of [our integrations](/docs/integrations).

<img alt="Event stream" />

You can sort your events either by email, period of time or event name. You can also drill down to specific events using the filters.

To view a specific event's payload, click on the table row. Here you'll see the incoming event's information, including the original sent event properties and the properties that Loops accepted (based on your event configuration).

<img alt="Viewing an individual event" />

## Testing events

You can test events by sending to email addresses with `@example.com` and `@test.com` domains (for example `user@example.com` and `user@test.com`).

Events will fire as normal but emails will not be sent to `@example.com` or `@test.com` email addresses, making this a good way to test how events are working in your account without affecting your sending domain’s reputation.


# Event properties
Source: https://loops.so/docs/events/properties

Learn how to use event properties to personalize your emails.

You can include additional data for each event with event properties, which can be used to include personalized information in emails triggered by your events.

Once included in an event, event properties can be added to emails within your workflows.

## Adding event properties

You can add event properties for your events from two places.

### Events page

Firstly, you can add them from the [Events page](https://app.loops.so/settings?page=events). Click on an event and then **+ Add event property**.

<img alt="Adding an event property" />

### Workflow builder

Secondly, you can add event properties from an **Event received** node in the workflow builder by clicking **Edit event properties** in the inspector panel on the right-hand side.

<img alt="Adding an event property in the workflow builder" />

### Important information about event properties

* Event properties must have unique names within the same event.
* Empty spaces are trimmed from the beginning and end of property names.
* Event properties can be one of four types:
  * String
  * Number
  * Boolean
  * Date
* Date values can be either a Unix timestamp (*in milliseconds*) or an [ECMA-262 date-time string](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format), similar to contact properties. [Read more](/docs/contacts/properties#dates)

## Editing event properties

You can add, rename, delete, and change data types for event properties in the event editor. Open it from the [Events page](https://app.loops.so/settings?page=events) or from the **Event received** node in the workflow builder, as above.

<img alt="Editing an event" />

## Sending event properties in an event

You can add event properties to your [Send event](/docs/api-reference/send-event) API request using `eventProperties`:

```json theme={"dark"}
{
  "email": "test@example.com",
  "eventName": "planChange",
  "firstName": "Sam",
  "favoriteColor": "red",
  "eventProperties": {
    "newPlan": "Pro",
    "oldPlan": "Basic",
    "planPrice": 29,
    "isLifetime": false,
    "updateDate": "2024-03-07T10:09:23Z"
  }
}
```

Conversion attempts are made for mismatched types (e.g. if you send a string for a number property).

<Warning>
  New properties not previously defined for an existing event will not be
  available when sending emails. Make sure you add event properties in Loops
  before including them in your event requests. If you send a request with a new
  `eventName` we will create a new event in your account and include the
  properties listed in `eventProperties`.
</Warning>

## Using event properties in emails

Event properties you create become available in emails once Loops has received events containing them.

In the email editor toolbar, click the `{}` icon (or type `{` in the editor) to open the dynamic content menu, then select the event property you want to add. Event properties are only available in emails within your workflows.

<img alt="Add an event property" />

In [LMX](/docs/creating-emails/lmx#event-properties), use `{event.<apiName>}` (for example, `{event.orderTotal}`) instead of the editor's `{EVENT_PROPERTY:...}` syntax.


# Custom form
Source: https://loops.so/docs/forms/custom-form

Integrate with Loops via a form endpoint, which will work with any type of custom form solution you have set up.

Our form endpoint lets you add new contacts to your audience from an HTML form or using JavaScript.

## Find the form endpoint

To submit data to Loops you will need to retrieve the form endpoint URL that's linked to your Loops account.

1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account.
2. Click on the **Settings** tab.
3. Copy the URL shown in the **Form Endpoint** field.

<img alt="Form settings showing the Loops form endpoint URL" />

## Create a form

In a form, add `input` elements for each of the contact properties you want to collect.

The following contact properties can be added to a new contact via a Loops form endpoint:

* Email `email`
* First name `firstName`
* Last name `lastName`
* User group `userGroup`
* Source `source` (default value is "Form" if omitted)
* Notes `notes`
* Mailing list subscriptions ([read more](/docs/forms/custom-form#subscribe-to-mailing-lists))

<Note>
  The only contact property that can be updated on an existing contact via a form endpoint is `userGroup`.\
  Existing contacts can be added to new mailing lists via a form, but not unsubscribed.
</Note>

You can also use any [custom contact properties](/docs/contacts/properties) from your [API settings page](https://app.loops.so/settings?page=api) in your form.

### Add contact properties

For each form field use the "API Name" value found from your [API settings page](https://app.loops.so/settings?page=api) as the `name` attribute.

<img alt="Contact properties table with API field names" />

Here's a simple example form that collects name, email address and assigns a custom user group:

```HTML theme={"dark"}
<form
  method="post"
  action="https://app.loops.so/api/newsletter-form/<YOUR_FORM_ID>"
>
  <input type="text" name="firstName" required>
  <input type="email" name="email" required>
  <input type="hidden" name="userGroup" value="Website signups">
  <button type="submit">Sign up</button>
</form>
```

### Subscribe to mailing lists

To add subscribers to mailing lists via a form endpoint, include a hidden field called `mailingLists`. You can use a single mailing list ID or to add subscribers to multiple lists, use a comma-separated list of mailing list IDs.

<Warning>
  Make sure that any mailing list you add to a form is
  [Public](/docs/contacts/mailing-lists#list-types). You cannot subscribe contacts to
  private mailing lists from the form endpoint.
</Warning>

```HTML {6} theme={"dark"}
<form
  method="post"
  action="https://app.loops.so/api/newsletter-form/<YOUR_FORM_ID>"
>
  <input type="email" name="email" required>
  <input type="hidden" name="mailingLists" value="cly2xnjbn002z0mme68uog1wk, cly4xnjbn002x0mme28uog1wk">
  <button type="submit">Sign up</button>
</form>
```

<Tip>
  If you need a hand integrating with your custom form, just [contact support](https://app.loops.so/settings?page=support) and we'll help you integrate with your specific setup.
</Tip>

## Submit with JavaScript

We recommend submitting forms using JavaScript because the endpoint responds with JSON.

To do this, make a `POST` request to your form endpoint. Make sure to set the `Content-Type` header to `application/x-www-form-urlencoded` and to submit the form body with encoded values.

<CodeGroup>
  ```javascript Simple example theme={"dark"}
  function handleSubmit() {
    const formBody = `firstName=${encodeURIComponent(firstName)}&email=${encodeURIComponent(email)}&mailingLists=${encodeURIComponent(mailingListIds)}`;

    // Change this URL to your own endpoint URL
    fetch("https://app.loops.so/api/newsletter-form/<YOUR_FORM_ID>", {
      method: "POST",
      body: formBody,
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
    });
  }
  ```

  ```javascript Detailed example theme={"dark"}
  function handleSubmit() {
    const formBody = `firstName=${encodeURIComponent(firstName)}&email=${encodeURIComponent(email)}&mailingLists=${encodeURIComponent(mailingListIds)}`;

    try {
      // Change this URL to your own endpoint URL
      const response = await fetch("https://app.loops.so/api/newsletter-form/<YOUR_FORM_ID>", {
        method: "POST",
        body: formBody,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });

      if (response.status === 429) {
        // Handle rate limit error
        return {
          success: false,
          message: "Too many signups, please try again in a little while"
        };
      }

      // Returns `success=true` or `success=false`, plus an error message if false
      const data = await response.json();
      return data;

    } catch (error) {
      return {
        success: false,
        message: "An error occurred. Please try again."
      };
    }
  }
  ```
</CodeGroup>

Responses from this form endpoint will be one of the following:

```json theme={"dark"}
HTTP 200

{
  success: true
}
```

```json theme={"dark"}
HTTP 400 or 500

{
  success: false,
  message: "A descriptive error message."
}
```

## Troubleshooting

Submissions to form endpoints are rate limited to a low number of requests per minute from each IP address. The endpoint will return a `HTTP 429` response if requests exceed the rate limit. You should try to handle these responses gracefully in your form as shown in the example above.


# Simple form
Source: https://loops.so/docs/forms/simple-form

Collect signups from any web page with a customizable form.

<Tip>
  Check out our specific documentation for adding forms to
  [Framer](/docs/integrations/framer) and [Webflow](/docs/integrations/webflow).
</Tip>

You may want to collect signups from your website directly into Loops.

## Generate the form

Easily generate an HTML or JSX form for collecting new signups for your mailing list from inside Loops.

Go to the [Forms page](https://app.loops.so/forms) and customize your form.

Here you will see settings to manage features of your form:

* add contacts to [mailing lists](/docs/contacts/mailing-lists) (make sure your lists are marked as **Public** first)
* add a "User group" value to each contact
* change the layout
* edit the button color
* edit the success message

When you're happy with your form, copy the HTML into your website.

<img alt="Loops simple form builder settings" />

## Add more fields to the form

It's possible to add other fields to your form if you want to collect more than just email addresses.

The following contact properties can be added to a new contact via a Loops form endpoint:

* Email `email`
* First name `firstName`
* Last name `lastName`
* User group `userGroup`
* Source `source` (default value is "Form" if omitted)
* Notes `notes`
* Mailing list subscriptions ([read more](/docs/forms/custom-form#subscribe-to-mailing-lists))

<Note>
  The only contact property that can be updated on an existing contact via a form endpoint is `userGroup`.\
  Existing contacts can be added to new mailing lists via a form, but not unsubscribed.
</Note>

When adding fields to a form, use the "API Name" value found from your [API settings page](https://app.loops.so/settings?page=api) as the `name` attribute for each field. See the above list for examples.

For example, this code would collect a First Name and add the contact to a mailing list (using a hidden field input that doesn't appear in the web page):

```html theme={"dark"}
<input type="text" name="firstName" />
<input type="hidden" name="mailingLists" value="cly2xnjbn002z0mme68uog1wk" />
```

If you want more flexibility when creating signup forms, [read about creating custom forms](/docs/forms/custom-form).

## Troubleshooting

Submissions to form endpoints are rate limited to a low number of requests per minute from each IP address. If you see the message "Too many signups, please try again in a little while" when submitting a form, you have hit the rate limit. Please wait and try again.

Additionally, the form JavaScript does not allow duplicates of the same email address per pageload.


# Building your first goal
Source: https://loops.so/docs/goals/building

Create a goal and attach it to a campaign

Goals are created and managed in the Loops dashboard. This guide walks through creating a goal and attaching it to a campaign. For the concepts behind eligibility, targets, and measurement windows, see the [Goals overview](/docs/goals/overview).

## Creating a goal

<Steps>
  <Step title="Navigate to Goals">
    Go to [Settings -> Goals](https://app.loops.so/settings?page=goals).
  </Step>

  <Step title="Create a new goal">
    Click **Create a goal** and give it a name. You can also add an optional description.
  </Step>

  <Step title="Set the eligibility filter (optional)">
    Use the filter builder to define which contacts are eligible to be enrolled. Leave it empty or toggle off the **Eligibility** section to make every contact who receives the campaign eligible.
  </Step>

  <Step title="Set the target filter">
    Use the filter builder in the **Conversion** section to define what success looks like, i.e. the contact state that should be considered as a conversion.
  </Step>

  <Step title="Set the default measurement window">
    Choose how many days contacts have to match the target filter after receiving a campaign.
  </Step>
</Steps>

## Attaching a goal to a campaign

<Steps>
  <Step title="Open a draft campaign">
    Goals can only be attached to campaigns that are still in **Draft**. Once a campaign has been sent or scheduled, its goal attachments are locked.
  </Step>

  <Step title="Attach a goal">
    In the campaign's **Send** page, select a goal from the goal picker.
  </Step>

  <Step title="(Optional) Override the measurement window">
    On the attachment, you can set a campaign-specific number of days.
  </Step>

  <Step title="Send the campaign">
    Contacts are evaluated for enrollment as the campaign delivers.
  </Step>
</Steps>

<Warning>
  **Goals cannot be attached retroactively.** Attach and detach only work on draft campaigns. Once a campaign is sending or sent, its goal attachments are fixed.
</Warning>

Goals are reusable: the same goal can be attached to multiple campaigns.

Once your campaign is sending, head to [Understanding metrics](/docs/goals/metrics) to read the results.


# Understanding metrics
Source: https://loops.so/docs/goals/metrics

Read goal metrics and the conversions chart

Once a campaign with an attached goal is sending, Loops tracks impressions, enrollments, and conversions. This guide explains how to read those numbers. For definitions of the underlying terms, see the [glossary in the Goals overview](/docs/goals/overview#glossary).

## Reading goal metrics

* **Contacts evaluated** and **contacts converted** count **unique contacts**. A person is counted at most once per goal.
* **Contacts enrolled** and **active / completed measurements** count **windows**. Every enrollment opens exactly one window, and a person could have multiple enrollments on the same goal.

The **conversions chart** plots first-time conversions per day, bucketed in **your browser's timezone**. A contact is never counted twice, and daily counts may differ by region.


# Overview
Source: https://loops.so/docs/goals/overview

Track conversions and measure the success of your campaigns

With goals, you can define specific outcomes that matter to your business and measure how effectively your campaigns drive those actions.

Ready to dive in? See [Building your first goal](/docs/goals/building) to create one and attach it to a campaign, or [Understanding metrics](/docs/goals/metrics) to learn how to read the results.

## How goals work

A goal defines what you're measuring: who counts as a candidate, what counts as success, and how long contacts have to get there. When you send a campaign with a goal attached, eligible contacts who receive it are enrolled and tracked for conversions.

A goal has three parts:

1. **Eligibility filter** (optional): Which contacts can be enrolled. If no eligibility filter is set, all contacts are eligible.
2. **Target filter** (required): What counts as achieving the goal, defined by contact property values.
3. **Measurement window**: How many days contacts have to convert after receiving the campaign.

## Goal enrollment

When a contact receives a campaign with an attached goal:

1. We check if they match the **eligibility filter** (if one is set).
2. We check if they already match the **target filter**.
3. **If they already match the target, they're not enrolled** — they've already achieved the goal.
4. Otherwise, they're enrolled and the measurement window starts.

## The measurement window

Once enrolled, we monitor each contact's properties. When a contact matches the target filter, they've achieved the goal and count as a conversion.

If a contact is ever observed matching the target filter during the window, they're marked as having converted, and that status sticks even if their properties later stop matching the filter.

## Editing a goal after it's live

Once a goal records its first impression, its core definition locks so your data stays consistent:

* **Locked:** eligibility filter, target filter.
* **Still editable:** name and description.

Need a different filter? Create a new goal for your next campaign.

## Glossary

| Term           | Definition                                                                                                                                                         |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Impression** | A contact received an email from a campaign with an attached goal.                                                                                                 |
| **Enrollment** | An eligible contact who wasn't already in the target state when they received the campaign. These contacts are tracked for the duration of the measurement window. |
| **Conversion** | An enrolled contact who matched the target filter within the measurement window.                                                                                   |

## Limitations

* **No causation proof.** Goals measure correlation between sending a campaign and a contact reaching the target state. To prove the email *caused* the conversion you'd need a holdout/control group, which goals don't provide today.

## Feedback

If you hit something unexpected or have a use case that doesn't fit, please reach out — your feedback directly shapes what ships next.


# Recipe: Active 30-day users segment
Source: https://loops.so/docs/guides/active-30-day-users

A reusable segment of contacts active in the last 30 days, ideal for warming a sender reputation and targeting engaged users with product updates.

The "active in the last 30 days" segment is a workhorse. It's the right audience for [product updates](/docs/guides/product-updates), warming [sender reputation](/docs/deliverability/sending-reputation), and testing new content without hitting less-engaged users.

This recipe sets up the segment, wires up the activity signal, and shows how to use it.

## What you need

* A `lastActive` (date) contact property, updated whenever a user takes a meaningful action
* An `isActive30d` (boolean) contact property that your app computes and writes (`true`/`false`)

## Writing the activity signal

Pick one or two actions that represent real usage, not every click. For most SaaS apps, logins and a core action (send, create, publish, message) are enough.

Using the [Update contact endpoint](/docs/api-reference/update-contact):

<CodeGroup>
  ```js API theme={"dark"}
  await fetch("https://app.loops.so/api/v1/contacts/update", {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOOPS_API_KEY}`,
    },
    body: JSON.stringify({
      email: user.email,
      userId: user.id,
      lastActive: Date.now(),
      isActive30d: true,
    }),
  });
  ```

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

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

  await loops.updateContact({
    email: user.email,
    userId: user.id,
    properties: {
      lastActive: Date.now(),
      isActive30d: true,
    },
  });
  ```
</CodeGroup>

The API accepts custom properties at the top level of the body. The [JavaScript SDK](/docs/sdks/javascript) nests them under `properties`.

After a contact already exists in Loops with a `userId`, you can send later updates with only `userId`.

You can also sync this property via [Segment](/docs/integrations/segment), [PostHog](/docs/integrations/posthog), or [RudderStack](/docs/integrations/rudderstack) if you already track these events.

<Note>
  `lastActive` needs to be a Date-type [contact property](/docs/contacts/properties).
  Send a millisecond timestamp and Loops will parse it.
</Note>

Because Loops filters do not support dynamic date expressions like "today minus 30 days," compute the rolling window in your app and write `isActive30d` directly.

Common patterns:

* **On activity events**: set `lastActive` to now (or a date you save in your database) and `isActive30d` to `true`
* **Daily backfill job**: once per day, set `isActive30d = (lastActive >= now - 30 days)` for all relevant contacts

This keeps segment membership accurate without relying on dynamic date math in the Loops UI.

Example daily backfill (Node.js + JavaScript SDK):

```ts syncToLoops.ts theme={"dark"}
import { LoopsClient } from "loops";

type UserActivityRow = {
  email: string;
  userId: string;
  lastActiveAtMs: number | null;
};

const loops = new LoopsClient(process.env.LOOPS_API_KEY!);
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;

async function syncActive30d(users: UserActivityRow[]) {
  const cutoff = Date.now() - THIRTY_DAYS_MS;

  for (const user of users) {
    const isActive30d =
      user.lastActiveAtMs !== null && user.lastActiveAtMs >= cutoff;

    await loops.updateContact({
      email: user.email,
      userId: user.userId,
      properties: {
        // Keep this in sync with your DB source of truth.
        lastActive: user.lastActiveAtMs,
        isActive30d,
      },
    });
  }
}
```

## Build the segment

Go to the [Audience page](https://app.loops.so/audience) and in the filters add:

* `isActive30d` `Is true`
* `Subscribed` `Is true`

Click **Save segment** and name it "Active 30-day users".

Because [segments update automatically](/docs/contacts/filters-segments#audience-segments), membership refreshes as `isActive30d` changes. You never need to rebuild it.

## Using the segment

### For product update campaigns

Send product updates to this segment instead of your full audience. Open rates will be substantially higher, which protects deliverability. See [Product updates](/docs/guides/product-updates) for copy guidance.

### For reputation warming

Early on, or after migrating domains ([guide](/docs/deliverability/migrating-domains)), this segment is the right audience for campaign #1. See [Your first onboarding emails](/docs/guides/onboarding-emails#targeted-campaign) for the full warming flow.

### As an audience filter inside a workflow

Inside a [workflow](/docs/workflows), apply the same filter on a send step. For example, a workflow triggered by **Event received** (such as a weekly feature-highlight event you fire from a cron job) can use this filter so the email only reaches engaged users. See the available [workflow triggers](/docs/workflows/triggers).

## Variations

* **7-day active**: add `isActive7d` as a separate boolean your app computes and writes (`true`/`false`).
* **Inactive (inverse)**: use `isActive30d` `Is false` for re-engagement. See [Churn risk segment](/docs/guides/churn-risk-segment).
* **Power users**: combine with a count property like `sessionCount30d` `Greater than` `10`.

## Read more

<CardGroup>
  <Card title="Filters and segments" icon="filter-list" href="/contacts/filters-segments" />

  <Card title="Contact properties" icon="list" href="/contacts/properties" />

  <Card title="Product update emails" icon="megaphone" href="/guides/product-updates" />

  <Card title="Sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Send emails from Bolt.new
Source: https://loops.so/docs/guides/bolt-emails

How to integrate Loops transactional emails into a Bolt application.

<Info>
  This guide is for Bolt but this kind of approach will work with many AI web
  builders, and also for different kinds of Loops integrations, like adding
  newsletter signup forms or sending events with the API instead of
  transactional emails.
</Info>

[Bolt](https://bolt.new) is an in-browser AI web development tool. This guide shows how you can integrate Loops into your Bolt project.

In this guide we will build a simple to-do application in Bolt, with a daily email digest sent with Loops. It uses Supabase for authentication and database.

Adding Loops sending to Bolt can be boiled down to four steps:

1. Prompt Bolt to add a Loops integration
2. Set up emails in Loops
3. Add your Loops credentials and IDs
4. Check the code for mistakes

<img alt="Bolt.new" />

## 1. Create the app with a prompt

I wrote a prompt to create the app. In this prompt I included information about using the Loops API.

In Bolt you can choose to add Loops to your original prompt or ask it to add Loops in a later prompt.

For example:

> I want to add daily reminder emails that should be sent at 6am every morning showing the new day's list of tasks. I will send these emails as transactional emails using the Loops API (loops.so)

Using this prompt, Bolt added code to my app that spoke to Loops and had everything ready for me to add my Loops credentials, including a Supabase [Edge Function](https://supabase.com/docs/guides/functions) that could be run on a cron job.

<img alt="Example Bolt application" />

<Info>
  To improve the code that Bolt produces, you can upload our full LLM text
  [available here](https://loops.so/docs/llms-full.txt) and append it to the
  message.
</Info>

## 2. Create transactional email in Loops

Once you have the basics of the app working, you can move over to Loops to set up the digest email.

This will be a [transactional email](/docs/transactional) (a 1:1 email for a single recipient), sent to each user every morning.

With Loops' transactional sending, you create the email body in the Loops editor and any dynamic content is added using [data variables](/docs/creating-emails/personalizing-emails). Then, when it comes to sending the email via the API, you include the dynamic content in the API payload.

<img alt="API call to Loops" />

In Loops, you'll need to create a new transactional email and write your content. Bolt added two data variables `tasks` and `date` to the code, so in my example I added those data variables to the email.

<img alt="Creating a transactional email in Loops" />

## 3. Add Loops credentials

As part of the integration with Loops, you will need to add a Loops API key to your code. This tells Loops which account is sending data as well as making sure the correct apps have access to the correct data.

To do this, get an API key from your Loops [API Settings](https://app.loops.so/settings?page=api) page.

For my example app I needed to add my Loops API key into the Edge Function's secrets in Supabase (**Edge functions -> Secrets**). I called mine `LOOPS_API_KEY` to match the name Bolt used in the code.

<img alt="Loops API key" />

<img alt="Supabase secret" />

You will also need the ID of the transactional email so that the correct email is sent from your application.

In Loops, from the **Preview** page of your transactional email, copy the transactional ID and paste it into your app's code. This completes the Loops integration in the app.

<img alt="Transactional ID" />

## 4. Fix bugs

Now that the app is set up, it's time to see if things are working correctly.

For me, I discovered a few bugs. For example, the original Google sign in that Bolt had added didn't work so I asked Bolt to change to email and password authentication. Overall the app worked as I had prompted. I could sign up and create tasks and the delay feature worked great.

<img alt="My new to-do app" />

Next I wanted to make sure the email sending was working. Because this was tied up in an edge function that is to be called with a scheduled cron job, I needed to find a way to test it on demand. Luckily, Supabase offers a simple API call to trigger functions. This can be run in the **Terminal** tab in Bolt.

<img alt="Invocation call" />

<img alt="Running a command in Bolt's terminal" />

Initially, I was seeing errors in the terminal.

```
{"success":true,"sent":0,"failed":1}
```

I realized that Bolt had incorrectly guessed the Loops API endpoint URL. This is a good example of AI-generated code needing sanity checking. I changed the URL to the [correct one](/docs/api-reference/send-transactional-email).

<Note>
  To help with this kind of AI hallucination you can upload or link to our
  [LLM-friendly docs file](https://loops.so/docs/llms-full.txt) within your
  prompts or chats.
</Note>

<img alt="API URL" />

Supabase has great visibility of edge function invocations and error messages, which can help figure out when there's a problem.

<img alt="Supabase logs" />

After a few tries, I started seeing successful invocations of the edge function, and email digests in my inbox!

<img alt="Example email in my inbox" />

Here you can see that the current date and the task I had scheduled for the same day have been passed into the email body, which was handled by the data variables in the API call to Loops.

That's the end of this guide. Make sure to check out the rest of the Loops API to see what is possible when integrating to a Bolt-powered application.

<CardGroup>
  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    View all of our API endpoints.
  </Card>

  <Card title="Creating emails in the editor" icon="keyboard" href="/creating-emails/editor">
    Learn how to create stylized emails and add personalization.
  </Card>

  <Card title="Transactional guide" icon="file-code" href="/transactional">
    Read our detailed guide for sending transactional emails.
  </Card>
</CardGroup>


# Bubble API Connector
Source: https://loops.so/docs/guides/bubble-api-connector

Send data to Loops from Bubble using the API Connector plugin.

This guide helps you set up an API connection to Loops from your Bubble app.

In this example, we will create an integration that allows you to sync Bubble users to your Loops audience.

<Tip>
  We have created an [official plugin for Bubble](/docs/integrations/bubble), which contains actions for all Loops API calls. However, it is limited to what it can do by how Bubble works.\
  This tutorial helps if you need more control or flexibility over the data that is sent to Loops.
</Tip>

## Install the API Connector plugin

In Bubble, you need to first install the API Connector Plugin. This plugin is what makes the API calls to Loops.

Go to Plugins and click **+ Add plugins**.

Search for "API Connector" and click **Install**.

<img alt="Search for API Connector" />

## Add a Loops API connection

Now you have the plugin installed, you can add an API connection.

Click **Add another API**. In the form that appears, enter "Loops" into the **API Name** field (Bubble uses this field to group API calls together in the interface).

Make sure the **Key name** value is "Authorization" (this is the default).

In the **Authentication** field, select "Private key in header".

In Loops, go to your [API Settings page](https://app.loops.so/settings?page=api) and copy an API key (you can create a new key if you like).

Back in Bubble, in the **Key value** field, write the word `Bearer`, then a space character, then paste your API key, so it looks something like `Bearer YourApiKey`.

<img alt="Add the Loops API" />

## Add an API call to sync users to Loops

Now you can create an API call using the connection you just created. Click **Add another call** at the bottom of the gray box.

In the **Name** field, write something like "Sync users".

In the **Use as** field, select "Action". The **Data type** field should be "JSON" (this is the default).

In the dropdown where it says "GET", select "POST", and in the subsequent field, paste the following endpoint URL:

```
https://app.loops.so/api/v1/contacts/update
```

<img alt="Add an API call" />

Now all that's left is to set up the request body, which is the data that's sent to Loops. This will depend on what kind of data points you want to send.

In this example, we will add a [custom contact property](/docs/contacts/properties) called "Plan name" (which we [already added in Loops](/docs/contacts/properties#add-a-property)).

<Warning>
  Due to how Bubble handles empty values, make sure to only add properties to this request body that will always contain a value. Otherwise, Bubble will send `"null"` as the value instead of an empty value, which will mean, for example, your contact's first name will become `null`.
</Warning>

To make the request, we have to include an email address (which is required to create new contacts in Loops). Then we'll also add a "User ID" value (which will maintain a distinct connection between a user in Bubble to their contact record in Loops) plus the "Plan name" property. Due to the data in my example application, I know all three of these fields will always have a value.

In the **Body** field, paste this:

```json theme={"dark"}
{
  "email": "<Email>",
  "userId": "<User_ID>",
  "planName": "<Plan_name>"
}
```

If you then click anywhere outside the **Body** field, you'll see two sets of fields appear below it, one for each of the three custom variables (`Email`, `User_ID` and `Plan_name`) we added to the body data.

Uncheck the **Private** checkbox for each of these variables, otherwise they won't show up in the Bubble interface when you come to use this API call.

<img alt="Adding body data" />

The final step is to initialize the call, which will show Bubble that the API call works.

To do this, enter some example values in each of the **Value** fields, then click **Initialize call** (this will send real data to Loops).

If it works, you'll see a success message and a confirmation of the returned values from Loops. Click **Save** to complete setting up the API call.

<img alt="Request success" />

If the request is not successful, you should see a message telling you what went wrong.

<Info>When you're done, remember to delete the test data you just added in the **Value** fields.</Info>

## Add the action to your Workflow

The final step is using your API call inside your application.

To do this, go to **Workflows** in your Bubble account and create a workflow for when you want to send a user to Loops.

In this example, I want to sync users to Loops whenever they log in (they could also be synced after other events, like if their email address changes).

To do this, click **Click here to add an event...** and select "General > User is logged in".

Click **Click here to add an action...**, hover over **Plugins** and select the action you created by the name you gave it (naming is organized by "API name - API call name").

<img alt="Select the API call" />

This will show a popover containing the fields you added in the previous step, allowing you to insert your data in Bubble into the API call.

Click into a field and then **Insert dynamic data**, then select the data you want to send to Loops. (In my example I searched for "Current User" and then selected `'s email`, `'s unique ID` and `'s plan` to fill out the three fields.)

<img alt="Add data to API call" />

Now when the selected event happens, Bubble will make an API call to Loops!

To verify everything is set up correctly, log in to your Bubble app (to trigger the event) and then check the [Audience page](https://app.loops.so/audience) in Loops to see if your user account appears.

## Other examples

There are lots of different ways you can use Bubble's API Connector to send data to Loops:

* Create a newsletter subscription form and send all submitted email addresses to Loops.
* Sync contacts whenever a user's email address is changed in Bubble.
* [Trigger an event](/docs/events) in Loops for all new sign ups using the [Send event endpoint](/docs/api-reference/send-event), to enter new users into a welcome email sequence.
* Periodically sync [contact properties](/docs/contacts/properties) to Loops so you can send personalized emails.

## Learn more

<CardGroup>
  <Card
    title="Bubble plugin"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17"><g fill="none" fillRule="nonzero" transform="translate(.5 .111)"><path fill="#FF4A00" d="M9.361 4.277c-1.474 0-2.928.633-4.037 1.877V.016H3.128v10.336a6.077 6.077 0 0 0 12.151 0c0-3.354-2.563-6.075-5.918-6.075Zm-.157 9.81a3.735 3.735 0 1 1 0-7.47 3.735 3.735 0 0 1 0 7.47Z"/><circle cx="1.53" cy="14.925" r="1.503" fill="#FF4A00"/></g></svg>
}
    href="/integrations/bubble"
  >
    Manage contacts and send emails with our Bubble plugin.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    View all of our API endpoints.
  </Card>

  <Card title="Contact properties" icon="address-book" href="/contacts/properties">
    Learn more about custom contact properties.
  </Card>
</CardGroup>


# Recipe: Churn risk segment and re-engagement workflow
Source: https://loops.so/docs/guides/churn-risk-segment

Define a churn risk segment based on inactivity signals, then automate a re-engagement sequence for those users.

Churn rarely happens suddenly. Users go quiet, skip a few sessions, stop getting value, and only then cancel. This recipe defines the segment that catches them early and a [workflow](/docs/workflows) that tries to pull them back.

## What you need

* A `lastActive` ([date contact property](/docs/contacts/properties))
* A `churnRisk` ([boolean contact property](/docs/contacts/properties)) your app computes and writes (`true`/`false`)
* Optional: a count property like `sessionCount30d` if you want tighter rules

These contact properties need to be updated via the [Update contact endpoint](/docs/api-reference/update-contact) or an [integration](/docs/integrations) when the user performs a key action.

For extended context on this pattern, [Sending a campaign to active users](/docs/guides/onboarding-emails#sending-a-campaign-to-active-users-in-workflows) is worth reading.

## Define the segment

Go to the [Audience page](https://app.loops.so/audience), apply filters, then save the [segment](/docs/contacts/filters-segments#audience-segments).

A reasonable starting definition:

* `churnRisk` `Is true`, AND
* `Subscribed` `Is true`

Set `churnRisk` from your app as:

* `true` when `lastActive` is between 14 and 60 days ago
* `false` otherwise

This keeps long-inactive users out so you do not wake up dormant signups, while also excluding brand-new contacts who have not had a chance to be active yet.

<Tip>
  Sending to users who have been inactive for months can hurt your [sender
  reputation](/docs/deliverability/sending-reputation). That's why this recipe caps
  the window at 60 days.
</Tip>

Because Loops filters do not support dynamic date expressions like "today minus 14 days," compute this window in your app or a daily sync job and write `churnRisk` directly.

```ts syncToLoops.ts theme={"dark"}
import { LoopsClient } from "loops";

type UserActivityRow = {
  email: string;
  userId: string;
  lastActiveAtMs: number | null;
};

const loops = new LoopsClient(process.env.LOOPS_API_KEY!);
const DAY_MS = 24 * 60 * 60 * 1000;
const CHURN_MIN_DAYS = 14;
const CHURN_MAX_DAYS = 60;

async function syncChurnRisk(users: UserActivityRow[]) {
  const now = Date.now();
  const minCutoff = now - CHURN_MIN_DAYS * DAY_MS;
  const maxCutoff = now - CHURN_MAX_DAYS * DAY_MS;

  for (const user of users) {
    const last = user.lastActiveAtMs;
    const churnRisk = last !== null && last <= minCutoff && last >= maxCutoff;

    await loops.updateContact({
      email: user.email,
      userId: user.userId,
      properties: {
        lastActive: last,
        churnRisk,
      },
    });
  }
}
```

## Set up the re-engagement workflow

<Steps>
  <Step title="Add a lifecycle property">
    Add a boolean [contact property](/docs/contacts/properties) in Loops called `churnRisk`.
    This should be updated via the [Update contact endpoint](/docs/api-reference/update-contact) whenever activity changes, or in a daily sync job.
  </Step>

  <Step title="Trigger on Contact updated">
    In the [workflow builder](/docs/workflows), pick **Contact updated** with
    `churnRisk` changing to `true`. See [Workflow
    triggers](/docs/workflows/triggers).
  </Step>

  <Step title="Add an audience filter">
    Match the trigger so only currently-at-risk users continue:
    `churnRisk` `Is true`.

    Choose between **One time** or **Every time** for the trigger frequency, depending on whether you want contacts to re-enter the workflow in the future.

    Set the filter scope to **All following nodes**, so Loops will continue evaluating each contact against the filter as the workflow progresses.
  </Step>

  <Step title="Build the email sequence">
    Add email nodes to the workflow, with timer nodes in between.

    * **Day 0**: "We noticed you have been quiet. Here is the one thing worth coming back for." Personal, useful, not promotional.
    * **Timer 4 days**
    * Send a customer story or missed-feature email.
    * **Timer 6 more days**
    * Send a direct ask with a link to cancel if they prefer.

    If you use **All following nodes** contacts will be checked against the filter before they reach each node, and will exit the workflow as soon as `churnRisk` is `false`.
  </Step>
</Steps>

## Why this works

* The segment updates automatically as `churnRisk` changes
* The filter in the workflow prevents messaging users who already came back
* The 60-day upper bound protects deliverability by keeping long-dormant users out

## Testing before you ship

* Send a test to your own account with `lastActive` manually set 20 days back and `churnRisk` set to `true`
* Use the [experiments feature](/docs/workflows/experiments) to A/B test subject lines if volume permits

## Read more

<CardGroup>
  <Card title="Lifecycle emails overview" icon="arrows-rotate" href="/guides/lifecycle-emails" />

  <Card title="Filters and segments" icon="filter-list" href="/contacts/filters-segments" />

  <Card title="Branching workflows" icon="code-branch" href="/workflows/branching" />

  <Card title="Sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Dark mode support in emails
Source: https://loops.so/docs/guides/email-dark-mode

Understanding dark mode support in email clients and best practices for consistent rendering.

It can be difficult to ensure consistent email rendering in both light and dark modes across different email clients. Dark mode is now a default setting for many email clients (on both desktop and mobile devices) and it's important to ensure your emails look good in both modes.

Email clients handle dark mode quite differently, making it challenging to maintain consistent branding and design. Some clients automatically invert colors, while others respect dark mode preferences through CSS media queries.

This guide explores the current state of dark mode support and provides practical solutions for maintaining brand consistency.

<Tip>
  While we don't currently support a dark mode version when creating emails in our editor, we automatically enable dark mode support in email clients that can handle it. Note that this doesn't include image handling, as dark mode support for images varies significantly across different email clients.
</Tip>

## Best practices

Ultimately, after experimenting with a number of techniques, the following combination of techniques can enhance consistency across clients while respecting dark mode preferences:

1. **Logo color**: use either gray or a colorful brand color that works on both light/dark backgrounds
2. **Image stroke**: apply a stroke to the logo that matches the light-mode email background
3. **Dark mode optimization**: Progressively enhance clients that support `prefers-color-scheme` with a specific dark mode logo

### Logo color

When designing logos for emails, try to use a color that works well in both light and dark modes. Consider using gray or brand colors that maintain visibility in dark mode.

Try to avoid dark or black logos that might disappear in dark mode. Make sure to test your emails in dark mode in different email clients to ensure the logo is visible.

<Frame>
  <img alt="Use gray or colored logos" />
</Frame>

Some email clients (like Gmail on Android 14) automatically invert small images under \~50px if they detect poor contrast in dark mode. This behavior varies across clients and versions, so it's crucial to test your specific logo in different clients.

### Image stroke

For logos that need to maintain their original colors, consider adding a stroke in the color of your light mode background. This stays invisible in light mode, but becomes visible in dark mode and highlights the logo.

Using thicker strokes usually improves visibility when scaled down. And ideally the change is made to the image itself rather than using a CSS filter.

Limitations to this technique:

* May not meet your brand or design guidelines
* You may need to make kerning or spacing changes for optimal appearance
* Stroke thickness might be limited by your logo's design
* Image scaling can affect stroke appearance and visibility

<Frame>
  <img alt="Stroked logo" />
</Frame>

### CSS

While [support](https://www.caniemail.com/features/css-at-media-prefers-color-scheme/) for the `prefers-color-scheme` media query is limited—with Gmail being the main barrier for mass adoption—you can still optimize for dark mode.

<Warning>
  Loops does not currently support custom CSS in our editor. You can still use this technique by adding the CSS to [imported MJML templates](/docs/creating-emails/uploading-custom-email).
</Warning>

With this technique you can use different image assets for dark mode in supported clients and have more control over text colors.

Apple Mail has the best dark mode support so it makes sense to test there first. Just make sure to also view your emails in Gmail and Outlook to ensure the dark mode is working as expected.

Here's a very basic example of how to use this technique:

```html theme={"dark"}
<style>
body {
  color: #000;
  background-color: #fff;
}
.light-logo {
  display: block;
}
.dark-logo {
  display: none;
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
  body {
    color: #fff;
    background-color: #000;
  }
  .light-logo {
    display: none;
  }
  .dark-logo {
    display: block;
  }
}
</style>

<img src="https://example.com/logo-light.png" alt="Logo" class="light-logo">
<img src="https://example.com/logo-dark.png" alt="Logo" class="dark-logo">
```

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understanding email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />
</CardGroup>


# Set up a welcome email sequence for new Ghost members
Source: https://loops.so/docs/guides/ghost-email-sequence

Send automated email sequences to new Ghost subscribers using Zapier and Loops.

This guide helps you set up email sequences that get sent to all new subscribers to your Ghost site.

With Loops, you can set up sophisticated sequences called [workflows](/docs/workflows), allowing you to send a range of welcome emails to your members over a period of time.

Using [Zapier](https://zapier.com), we can automatically add every new member who signs up on your Ghost site to your Loops audience.

## Set up the Zapier Trigger

The first step is to connect your Ghost site to Loops using Zapier.

Sign up to Zapier and create a new Zap using Ghost's **Member Created** Trigger.

<img alt="Add Ghost trigger" />

This creates an automation that will trigger every time a new member is created in Ghost. Zapier will then send the contact's information over to Loops.

Now connect Ghost from the Trigger by clicking **Account** then **Sign in** and then pasting in your Ghost API key and API URL. To find these, go to Settings in your Ghost admin, search for "Integrations" and click on **Zapier**.

<img alt="Sign in to Ghost" />

## Set up the Zapier Action

Next you need to link up Loops as the Action. Click the **Action** node, search for Loops and select the **Send Event** option in the Event dropdown.

Click **Continue**, then **Sign in** and paste in your Loops API key (which you can find from your [API Settings page](https://app.loops.so/settings?page=api)).

<img alt="Sign in to Loops" />

<Tip>
  Instead of using an event for the workflow trigger, you could also choose to send the workflow to every new contact created in your audience.
  We choose the event trigger in this example as you may add other contacts to your audience from other sources than Ghost.
</Tip>

Now click **Continue** to move to the **Action** tab. This is where you can configure which Ghost data is sent to Loops.

As you click each field, you can select the different Ghost-provided data.

In **Email**, select the Email field. In **User ID**, select the ID field.

In the **Event Name** field, write something like `newMember`. This is the name we'll use in Loops to trigger the email sequence. You can use any name, but make it descriptive. You'll need this name in the next step inside Loops.

<img alt="Adding an event name" />

If you want to include contact-related information in your emails you can use [event properties](/docs/events/properties). To do this, add more Ghost data in the **Event Properties** field. For example, if you want to include the user's name, subscribed status, member status or your newsletter's name, you can add these properties here. Click the `+` button to add new properties each time.

<img alt="Adding event properties" />

Click **Continue** to see an overview of the event and its data that you'll send to Loops. Here you can test the action works by clicking **Test Step**. This will send actual data to Loops, which you will see in the Event Log section on the [Events page](https://app.loops.so/settings?page=events).

When you're happy everything is set up properly, click **Publish**.

<Tip>
  If you ever change the event properties sent from Zapier, you need to update the event data in Loops to match. You can do this from [Settings -> Events](https://app.loops.so/settings?page=events). Click on the event and edit the listed properties. [More info](/docs/events/properties#editing-event-properties)
</Tip>

## Create an email sequence

Now that the connections are set up, you can create the email workflow in Loops.

Go to Loops and click on **Workflows** in the sidebar.

Click **New**, which will create a new workflow and show the [workflow builder](/docs/workflows).

Select the **Event is fired** trigger option.

Click on the **Event received** trigger in the workflow builder and enter the name you entered in Zapier in the inspector panel on the right-hand side (in this example, `newMember`).

<img alt="Select the event trigger" />

You can click the **Timer** node and edit it in the inspector panel if you want to add a delay between the event being received by Loops and the email being sent to your Ghost members.

<img alt="Set a timer delay" />

You can also add a filter to the audience for this workflow, if you want to limit sending to a certain sub-group of your members. If you want the workflow to send to all new Ghost members, just leave it as "All contacts".

Lastly, click the **Send email** node and then **Create email** in the inspector panel. You'll see the [email editor](/docs/creating-emails/editor) open, where you can create your email.

If you opted to send event properties from Zapier, you can add them to your email by clicking the `{}` icon above the editor (or typing `{` in the editor) and selecting the event property from the dynamic content menu.

<img alt="Adding event properties in the editor" />

Once your first email is complete, you can add more Timer and Email nodes to your workflow to complete your email sequence. Just click on the `+` icon between nodes to add new ones.

## Learn more

<CardGroup>
  <Card title="Workflows" icon="arrows-rotate" href="/workflows">
    Read more about triggering emails with events.
  </Card>

  <Card title="Creating emails in the editor" icon="keyboard" href="/creating-emails/editor">
    Learn how to create stylized emails and add personalization.
  </Card>

  <Card
    title="Zapier"
    icon={
  <svg
    viewBox="0 0 256 256"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
  >
    <g>
      <path
        d="M128.080089,-0.000183105 C135.311053,0.0131003068 142.422517,0.624138494 149.335663,1.77979593 L149.335663,1.77979593 L149.335663,76.2997796 L202.166953,23.6044907 C208.002065,27.7488446 213.460883,32.3582023 218.507811,37.3926715 C223.557281,42.4271407 228.192318,47.8867213 232.346817,53.7047992 L232.346817,53.7047992 L179.512985,106.400063 L254.227854,106.400063 C255.387249,113.29414 256,120.36111 256,127.587243 L256,127.587243 L256,127.759881 C256,134.986013 255.387249,142.066204 254.227854,148.960282 L254.227854,148.960282 L179.500273,148.960282 L232.346817,201.642324 C228.192318,207.460402 223.557281,212.919983 218.523066,217.954452 L218.523066,217.954452 L218.507811,217.954452 C213.460883,222.988921 208.002065,227.6115 202.182208,231.742607 L202.182208,231.742607 L149.335663,179.04709 L149.335663,253.5672 C142.435229,254.723036 135.323765,255.333244 128.092802,255.348499 L128.092802,255.348499 L127.907197,255.348499 C120.673691,255.333244 113.590195,254.723036 106.677048,253.5672 L106.677048,253.5672 L106.677048,179.04709 L53.8457596,231.742607 C42.1780766,223.466917 31.977435,213.278734 23.6658953,201.642324 L23.6658953,201.642324 L76.4997269,148.960282 L1.78485803,148.960282 C0.612750404,142.052729 0,134.946095 0,127.719963 L0,127.719963 L0,127.349037 C0.0121454869,125.473817 0.134939797,123.182933 0.311311815,120.812834 L0.36577283,120.099764 C0.887996182,113.428547 1.78485803,106.400063 1.78485803,106.400063 L1.78485803,106.400063 L76.4997269,106.400063 L23.6658953,53.7047992 C27.8076812,47.8867213 32.4300059,42.4403618 37.4769335,37.4193681 L37.4769335,37.4193681 L37.5023588,37.3926715 C42.5391163,32.3582023 48.0106469,27.7488446 53.8457596,23.6044907 L53.8457596,23.6044907 L106.677048,76.2997796 L106.677048,1.77979593 C113.590195,0.624138494 120.688946,0.0131003068 127.932622,-0.000183105 L127.932622,-0.000183105 L128.080089,-0.000183105 Z M128.067377,95.7600714 L127.945335,95.7600714 C118.436262,95.7600714 109.32891,97.5001809 100.910584,100.661566 C97.7553011,109.043534 96.0085811,118.129275 95.9958684,127.613685 L95.9958684,127.733184 C96.0085811,137.217594 97.7553011,146.303589 100.923296,154.685303 C109.32891,157.846943 118.436262,159.587052 127.945335,159.587052 L128.067377,159.587052 C137.576449,159.587052 146.683802,157.846943 155.089415,154.685303 C158.257411,146.290368 160.004131,137.217594 160.004131,127.733184 L160.004131,127.613685 C160.004131,118.129275 158.257411,109.043534 155.089415,100.661566 C146.683802,97.5001809 137.576449,95.7600714 128.067377,95.7600714 Z"
        fill="#FF4A00"
        fillRule="nonzero"
      ></path>
    </g>
  </svg>
}
    href="/integrations/zapier"
  >
    Manage contacts and send emails from thousands of other platforms.
  </Card>
</CardGroup>


# Why we don't support HTML emails
Source: https://loops.so/docs/guides/html-emails

Understanding why Loops doesn't support HTML email content and the benefits of our approach.

This guide explains why Loops doesn't support HTML email content in your requests, and how this approach benefits your team in the long run. We'll cover the common questions we receive and explain our philosophy on email management.

## The multi-provider problem

In our experience working with hundreds of software companies, we've noticed a pattern: by the Series B stage, most companies are using 3-6 different email sending providers. This fragmentation happens because different teams have different needs:

* Growth teams need advanced automation tools
* Marketing teams focus on promotional content
* Product teams send surveys and feature-specific emails
* Sales teams need targeted outreach
* Customer success teams handle support emails
* Engineering teams want simple, maintainable solutions

Each department gets its own tool, requiring coordination between design, marketing, legal, and engineering to keep everything working together.

**We believe there's a better way.**

## Why we don't support HTML content

We intentionally don't support HTML content in your requests because we don't believe emails belong in your codebase. Here's why:

### Branding management

When you need to update your brand colors, footer copy, or logo, you shouldn't need to:

1. Open a pull request
2. Update code
3. Deploy changes
4. Test across all email types

In Loops, these changes are a single click away and apply instantly across all your emails, whether they're transactional, marketing, or product updates. Even better, non-technical team members can make these changes without involving engineering.

### Email client compatibility

Email clients are notoriously inconsistent in how they render HTML. We maintain daily updates to our email rendering system to handle things like:

* New email client versions
* Dark mode changes
* Mobile rendering quirks
* Legacy client support

By handling this complexity for you, we ensure your emails look great everywhere, from the latest iPhone to that ancient Outlook version your biggest client refuses to update.

### Long-term support

Email clients evolve slowly but constantly. Keeping up with that is literally our full-time job. By taking HTML out of your codebase, we're basically giving you a free email-compatibility team that monitors email client changes, updates, tests across all major clients, and handles edge cases.

### Security and deliverability

When major email providers like Gmail and Yahoo update their guidelines, we catch changes early and update our system proactively, ensuring your emails remain compliant and maintain high deliverability rates.

### Team collaboration

Keeping your emails out of your codebase means any member of your team can make changes without engineering involvement.

It's also possible to preview new content or designs without affecting live emails, and make changes instantly without deployment.

## Summary

You probably have better things to do than manage email templates. Our approach means:

* Your team can update emails without engineering
* Changes are instant and safe
* Emails look great everywhere
* You maintain consistent branding
* We handle all the technical complexity

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understanding email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />
</CardGroup>


# Guides for onboarding, deliverability, and lifecycle emails.
Source: https://loops.so/docs/guides/intro

Browse Loops guides on onboarding, deliverability, lifecycle emails, integrations, and more.

## Getting started

Start sending email with the best results.

<CardGroup>
  <Card title="Onboarding emails" icon="list-ol" href="/guides/onboarding-emails" />
</CardGroup>

## Deliverability

Improve your email deliverability with these guides.

<CardGroup>
  <Card title="DMARC and DKIM setup" icon="lock" href="/deliverability/dmarc-dkim-setup" />

  <Card title="Optimization" icon="chart-line" href="/deliverability/optimization" />

  <Card title="Improving inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Sending to large audience" icon="users" href="/deliverability/sending-to-large-audience" />

  <Card title="Sending from subdomain" icon="server" href="/deliverability/sending-from-subdomain" />

  <Card title="Build your sender reputation" icon="shield" href="/deliverability/sending-reputation" />

  <Card title="Sending transactional emails to group inboxes" icon="user-group" href="/deliverability/transactional-group-inboxes" />

  <Card title="Gaining insights" icon="magnifying-glass-chart" href="/deliverability/gaining-insights" />

  <Card title="Understanding email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Adding a sending avatar" icon="user" href="/deliverability/adding-a-sending-avatar" />

  <Card title="Maintaining a clean list" icon="broom" href="/deliverability/maintaining-a-clean-list" />

  <Card title="Sending emails to Apple's 'Hide my email' addresses" icon="eye" href="/deliverability/apple-hide-my-email" />
</CardGroup>

## Integrations

How-to guides for connecting Loops to other platforms.

<CardGroup>
  <Card title="Use the Loops SDK in Next.js" href="/guides/nextjs" icon="js" />

  <Card
    title="Send Better Auth emails with Loops"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><path fill="#FF4A00" d="M69 121h86.988v259H69zM337.575 121H430v259h-92.425z"/><path fill="#FF4A00" d="M427.282 121v83.456h-174.52V121zM430 296.544V380H252.762v-83.456z"/><path fill="#FF4A00" d="M252.762 204.455v92.089h-96.774v-92.089z"/></svg>}
    href="/guides/better-auth-emails"
  />

  <Card title="Send emails from Bolt.new" icon="bolt" href="/guides/bolt-emails" />

  <Card
    title="Bubble API Connector"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="16"
    height="17"
    viewBox="0 0 16 17"
  >
    <g fill="none" fillRule="nonzero" transform="translate(.5 .111)">
      <path
        fill="#FF4A00"
        d="M9.361 4.277c-1.474 0-2.928.633-4.037 1.877V.016H3.128v10.336a6.077 6.077 0 0 0 12.151 0c0-3.354-2.563-6.075-5.918-6.075Zm-.157 9.81a3.735 3.735 0 1 1 0-7.47 3.735 3.735 0 0 1 0 7.47Z"
      />
      <circle cx="1.53" cy="14.925" r="1.503" fill="#FF4A00" />
    </g>
  </svg>
}
    href="/guides/bubble-api-connector"
  />

  <Card title="Email sequence for Ghost" icon="list-check" href="/guides/ghost-email-sequence" />

  <Card
    title="Send email from Next.js with Loops"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    preserveAspectRatio="xMidYMid"
    viewBox="0 0 256 256"
  >
    <path
      fill="#FF4A00"
      d="M119.617.069c-.55.05-2.302.225-3.879.35-36.36 3.278-70.419 22.894-91.99 53.044-12.012 16.764-19.694 35.78-22.597 55.922C.125 116.415 0 118.492 0 128.025c0 9.533.125 11.61 1.151 18.64 6.957 48.065 41.165 88.449 87.56 103.411 8.309 2.678 17.067 4.504 27.027 5.605 3.879.425 20.645.425 24.524 0 17.192-1.902 31.756-6.155 46.12-13.486 2.202-1.126 2.628-1.426 2.327-1.677-.2-.15-9.584-12.735-20.845-27.948l-20.47-27.648-25.65-37.956c-14.114-20.868-25.725-37.932-25.825-37.932-.1-.025-.2 16.84-.25 37.431-.076 36.055-.1 37.506-.551 38.357-.65 1.226-1.151 1.727-2.202 2.277-.801.4-1.502.475-5.28.475h-4.33l-1.15-.725a4.679 4.679 0 0 1-1.677-1.827l-.526-1.126.05-50.166.075-50.192.776-.976c.4-.525 1.251-1.2 1.852-1.526 1.026-.5 1.426-.55 5.755-.55 5.105 0 5.956.2 7.282 1.651.376.4 14.264 21.318 30.88 46.514 16.617 25.195 39.34 59.599 50.5 76.488l20.27 30.7 1.026-.675c9.084-5.905 18.693-14.312 26.3-23.07 16.191-18.59 26.626-41.258 30.13-65.428 1.026-7.031 1.151-9.108 1.151-18.64 0-9.534-.125-11.61-1.151-18.641-6.957-48.065-41.165-88.449-87.56-103.411-8.184-2.652-16.892-4.479-26.652-5.58-2.402-.25-18.943-.525-21.02-.325Zm52.401 77.414c1.201.6 2.177 1.752 2.527 2.953.2.65.25 14.562.2 45.913l-.074 44.987-7.933-12.16-7.958-12.16v-32.702c0-21.143.1-33.028.25-33.603.4-1.401 1.277-2.502 2.478-3.153 1.026-.525 1.401-.575 5.33-.575 3.704 0 4.354.05 5.18.5Z"
    />
  </svg>
}
    href="/sdks/javascript/nextjs"
  />
</CardGroup>

## Creating emails

How to create different kinds of emails in Loops.

<CardGroup>
  <Card title="Create a scheduled digest" icon="hourglass-end" href="/guides/scheduled-digest-email" />

  <Card title="Create a product update" icon="star" href="/guides/product-updates" />

  <Card title="Send to new subscribers again" icon="envelope-open-text" href="/guides/send-again" />

  <Card title="Create lifecycle emails" icon="users-between-lines" href="/guides/lifecycle-emails" />

  <Card title="Dark mode support" icon="moon" href="/guides/email-dark-mode" />
</CardGroup>

## Email basics

Learn more about email.

<CardGroup>
  <Card title="What is DNS?" icon="globe" href="/guides/what-is-dns" />

  <Card title="What is BIMI?" icon="circle-user" href="/guides/what-is-bimi" />

  <Card title="Open rates are a vanity metric" icon="envelope-open" href="/guides/open-rates" />
</CardGroup>

## Docs blog

* [How we create our documentation](/docs/guides/how-we-work-documentation)


# Recipe: SaaS customer lifecycle emails
Source: https://loops.so/docs/guides/lifecycle-emails

How to send onboarding, dunning and churn emails to your customers with workflows.

For SaaS companies, there are a few important events in a customer lifecycle that email can help with.

* **Acquisition emails** are emails sent after a user signs up for a waitlist, platform or newsletter.
* **Onboarding emails** help brand new users get familiar with—and get the most out of—your product.
* **Retention emails** keep your users engaged long-term.
* **Re-engagement emails** attempt to get users active again if they haven't used your platform recently.
* **Dunning emails** help reduce churn by prompting the user to re-activate a subscription or fix payment issues.
* **Re-activation emails** attempt to bring users back after a cancellation.

With Loops, you can easily set up workflows plus our API (or an integration) to help with each of these use cases.

## How it works

To get this set up in Loops, the idea is to create a [workflow](/docs/workflows) for each of the types of email you want to send. So there would be a workflow for activation emails and a workflow for dunning emails and so on.

A workflow is like an email sequence containing emails, time delays, audience filters and an initial trigger.

We will use a [custom contact property](/docs/contacts/properties) called `subscriptionStatus` to enter users into each of the workflows at different times in their subscriptions.

This property is used as the [workflow trigger](/docs/workflows/triggers); if the property is ever changed using [an integration](/docs/integrations) or the [Loops API](/docs/api-reference/intro), we trigger a workflow, which will send emails to the contact.

## Add a contact property

First, let's add the contact property to the audience in Loops.

Go to your [Audience page](https://app.loops.so/audience). Click on a table header to show the dropdown then select **Add property**.

Enter "Subscription status" into the **Name** field and make sure **String** is selected in the "Type" field. (You'll notice that the "stored name" of your property is `subscriptionStatus`. This is the name we'll use in the API and integrations).

Click **Add Property** when you're done. Your new property was added to the far right of the Audience table.

## Update contacts

Now you can click on contacts and update the value manually one-by-one, or use the API or an integration to update contacts programmatically.

To update contacts in bulk you can download your Audience as a CSV, update values and [re-upload the file](/docs/add-users/csv-upload).

## Create workflows

The next step is to create the workflows, one for each type of email. Repeat the following step for each of the different subscription statuses you want to send emails for.

Go to the [Workflows page](https://app.loops.so/workflows) in your account and click **New**.

Select the **Contact update** option, which will create a new workflow using that trigger.

<img alt="Select trigger" />

You will enter the [workflow builder](/docs/workflows). Click on the **Contact updated** node and set up the workflow trigger in the inspector panel on the right-hand side.

### Set up the trigger

1. Select the "Subscription Status" property from the **When** dropdown.
2. The **Changes from** dropdown should have "Any value" selected.
3. Next you need to specify what value Loops should look out for to enter users into the workflow.
   For example, for onboarding emails, you can choose "is empty" from the **To** dropdown. For re-activation emails select "Equals" from the **To** dropdown and enter a relevant value like "Canceled".
4. **Trigger time** should be set to "Every time"; this will make sure users are entered into the workflow every time that the Subscription Status value matches.

### Create emails

Now create the email(s) you want to send from each workflow.

You can add as many emails as you like into each workflow, separated with timers. If you want to send emails to certain groups of contacts, you can use Audience filters in your workflow.

We have a range of [email templates](https://app.loops.so/templates) available, which you can use as a base for all of your subscription emails.

<img alt="Templates" />

If you want to use cohesive branding in your emails, make use of [themes](/docs/creating-emails/editor#themes) in the editor.

<img alt="Editor panel" />

## Update the Subscription status value

The final step is to make sure that your contacts have the correct "Subscription status" value assigned to them in Loops.

This will make sure they are in the correct workflows at the correct moment within their subscription.

Using the API you can make a simple `PUT` call to the [Update contact endpoint](/docs/api-reference/update-contact), with a request containing the contact's email address and subscription status.

(You can also add more contact properties in this call if you want to add more data to the contact in Loops.)

<Note>
  Using the Update contact endpoint will either create or update a contact in
  Loops using the provided email address.
</Note>

```json theme={"dark"}
{
  "email": "test@example.com",
  "subscriptionStatus": "Canceled"
}
```

Many of [our integrations](/docs/integrations) allow syncing of contact data. Just make sure you create or update contacts and include the current `subscriptionStatus` value.

Now you have set up different workflows to trigger when the "Subscription status" value changes, your users will be automatically entered into each workflow and receive the correct emails at the correct times during their subscription lifecycle.

## Example workflows

Here are some example workflows you can create along with a suggested trigger and workflow contents.

### Onboarding

Sent to brand new users.

* **Trigger**: Contact added or Contact updated (e.g. `subscriptionStatus` is empty).
* **Audience filter**: Match the trigger (e.g. `subscriptionStatus` is empty).
* **Workflow contents**: 1–5 welcome and onboarding emails over the first 30 days.

### New subscribers

Sent to users who just started paying.

* **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Paying").
* **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Paying").
* **Workflow contents**: 1–3 emails about paid-only features over the next 3 days.

### Dunning

Sent to customers who had a first failed payment.

* **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Failed").
* **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Failed").
* **Workflow contents**: 1–3 emails during your dunning period.

### Churn

Sent to customers who have just canceled their payments.

* **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Canceled")
* **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Canceled").
* **Workflow contents**: 1 email saying goodbye and asking for feedback.


# Set up Loops in Next.js
Source: https://loops.so/docs/guides/nextjs

How to send email from your Next.js project with Loops.

This guide shows how to add Loops to your Next.js project, so you can send transactional emails, manage contacts and trigger automated emails.

## Install the SDK

The first step is to install the [Loops SDK](/docs/sdks/javascript). This is written in TypeScript so you can benefit from strict types when coding.

```bash theme={"dark"}
npm i loops
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You'll need an API key to use the SDK. Go to your [API Settings page](https://app.loops.so/settings?page=api) in Loops to generate and copy a key.

Save this value in your environment variables as something like `LOOPS_API_KEY`.

Then you can import the Loops SDK client like this:

```javascript theme={"dark"}
import { LoopsClient } from "loops";

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

<Tip>
  You can also use the [Loops API](/docs/api-reference/intro) directly in your app, without
  the SDK. [Read more](#using-the-api-instead)
</Tip>

<CardGroup>
  <Card title="JavaScript SDK" href="/sdks/javascript" icon="js">
    Explore our official JS/TS SDK.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>

## Server-side only

It is important that you only use the Loops API and SDK from server-side code. If you make calls directly in the browser, you risk exposing your API key, which would give other people read and write access to your Loops account data. Additionally, the Loops API does not support cross-origin requests made from client-side JavaScript.

If you want to make calls from the browser—for example, to collect newsletter subscriptions from a form—create proxy endpoints. To add a new contact, create an internal API endpoint and use the Loops API/SDK within it.

```typescript app/api/contacts/route.ts theme={"dark"}
import { NextRequest, NextResponse } from "next/server";
import { LoopsClient } from "loops";

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

export async function POST(request: NextRequest) {
  const res = await request.json();

  const email = res["email"];

  // Note: updateContact() will create or update a contact
  const resp: {
    success: boolean;
    id?: string;
    message?: string;
  } = await loops.updateContact(email);

  return NextResponse.json({ success: resp.success });
}
```

## Send transactional email

A big use case for using Loops in a Next.js project is to send transactional email to users. These emails are one-off emails, which help users with your product, for example password reset emails and notification emails.

To create a transactional email, go to the Transactional page in Loops. Click **Create** or select a template.

Create the email in [the editor](/docs/creating-emails/editor), which gives you rich formatting options and components.

<img alt="Creating a transactional email" />

To add dynamic content (like reset password URLs or user data) you can add [data variables](/docs/transactional#add-data-variables) into the email from the toolbar.

Give each data variable a unique name. You can populate these variables from your code when sending the email via the SDK in the next step.

Make sure to Publish your transactional email when you're done.

Now your email is created you can start sending emails.

In your code, call `sendTransactionalEmail()` and include values for each of the data variables you added to your email.

<CodeGroup>
  ```javascript JavaScript theme={"dark"}
  const dataVariables = {
    loginUrl: "https://myapp.com/login/",
  };

  const resp = await loops.sendTransactionalEmail({
    transactionalId: "transaction_email_id",
    email: "test@example.com",
    dataVariables
  });

  if (!resp.success) {
    // The sending failed
  } else {
    // The email was sent successfully
  }

  ```

  ```typescript TypeScript theme={"dark"}
  const dataVariables: { loginUrl: string } = {
    loginUrl: "https://myapp.com/login/",
  };

  const resp: {
    success: boolean,
    path?: string,
    message?: string
  } | {
    success: false;
    error: {
      path: string;
      message: string;
    };
    transactionalId?: string;
  } = await loops.sendTransactionalEmail({
    transactionalId: "transaction_email_id",
    email: "test@example.com",
    dataVariables
  });

  if (!resp.success) {
    // The sending failed
  } else {
    // The email was sent successfully
  }
  ```
</CodeGroup>

The response will contain a `success` boolean telling you if the email was sent successfully. If it was not, you'll also receive an error message.

<CardGroup>
  <Card title="sendTransactionalEmail()" href="https://github.com/loops-so/loops-js#sendtransactionalemail" icon="code">
    Read more in the SDK docs.
  </Card>
</CardGroup>

## Sync users to Loops

Another main use case for teams using Loops is to keep their Loops audience updated when user data changes in their application.

To do this you can use the `updateContact()` method.

<Tip>
  `updateContact()` can be used as a shortcut "update or create" function. It
  will create new contacts if the provided email address and/or user ID are not
  found.
</Tip>

For example, you may store custom data in Loops like subscription plan level or user usage information that you include in emails. You can update contacts in Loops like this:

<CodeGroup>
  ```javascript JavaScript theme={"dark"}
  const contactProperties = {
    userId: 826,
    planName: "Pro" /* Custom property */,
    usage: 172629 /* Custom property */,
  };

  const resp = await loops.updateContact("test@example.com", contactProperties);

  if (!resp.success) {
  // The call failed
  } else {
  // The contact was updated OK
  }

  ```

  ```typescript TypeScript theme={"dark"}
  const contactProperties: Record<string, string | number> = {
    userId: 826,
    planName: "Pro" /* Custom property */,
    usage: 172629 /* Custom property */,
  };

  const resp: {
    success: boolean,
    id?: string,
    message?: string
  } = await loops.updateContact("test@example.com", contactProperties);

  if (!resp.success) {
    // The call failed
  } else {
    // The contact was updated OK
  }
  ```
</CodeGroup>

The TypeScript example above shows how to properly type your `contactProperties` object and the expected response from the `updateContact` method.

<Tip>
  We recommend always populating the `userId` value for users, which should be
  their unique value in your platform. This allows you to change a contact's
  email address in the future, because they have a separate unique identifier in
  the system.
</Tip>

<CardGroup>
  <Card title="updateContact()" href="https://github.com/loops-so/loops-js#updatecontact" icon="user">
    Read more in the SDK docs.
  </Card>
</CardGroup>

## Trigger workflows with events

A third example of using the Loops SDK is to trigger [workflows](/docs/workflows). Workflows are automated email sequences, which can send multiple emails to contacts.

You can trigger these emails using [events](/docs/events), and you can send events to Loops using the SDK.

For example, you may have a workflow that you send to new users after they have completed an onboarding flow in your app.

First, [create a new workflow](https://app.loops.so/workflows) using the "Event received" trigger. Add emails, timers and audience filters to your workflow as you wish.

Then to trigger this email sequence, send an event to Loops. If your event name is `completedOnboarding`, your call would look like this...

<CodeGroup>
  ```javascript JavaScript theme={"dark"}
  const resp = await loops.sendEvent({
    email: "test@example.com",
    eventName: "completedOnboarding",
  });

  if (!resp.success) {
  // The event was not sent
  } else {
  // The event was sent OK
  }

  ```

  ```typescript TypeScript theme={"dark"}
  const resp: {
    success: boolean,
    message?: string,
  } = await loops.sendEvent({
    email: "test@example.com",
    eventName: "completedOnboarding",
  });

  if (!resp.success) {
    // The event was not sent
  } else {
    // The event was sent OK
  }
  ```
</CodeGroup>

<CardGroup>
  <Card title="sendEvent()" href="https://github.com/loops-so/loops-js#sendevent" icon="bolt">
    Read more in the SDK docs.
  </Card>
</CardGroup>

## Using the API instead

If you prefer, you can use the [Loops API](/docs/api-reference/intro) directly instead of using the SDK.

<Warning>
  You should never call the API from your front-end code as this will expose
  your API key.
</Warning>

For example, you can send a transactional email like this:

```javascript theme={"dark"}
const data = {
  email: "test@example.com",
  transactionalId: "abcdefg",
  dataVariables: {
    loginUrl: "https://myapp.com/login/?code=1234",
  },
};
return fetch("https://app.loops.so/api/v1/transactional", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.LOOPS_API_KEY}`,
  },
  body: JSON.stringify(data),
})
  .then((response) => response.json())
  .then((response) => {
    if (!response.success) {
      // The sending failed
    } else {
      // The email was sent successfully
    }
  })
  .catch((err) => console.error(err));
```

<Tip>
  On Vercel, each backend function gets its own lambda. Make sure you use
  `return` otherwise the lambda might be terminated before the promise is
  evaluated.
</Tip>

<CardGroup>
  <Card title="API Reference" icon="rectangle-terminal" href="/api-reference/intro">
    Read through our API documentation.
  </Card>
</CardGroup>


# Recipe: Onboarding completion workflow
Source: https://loops.so/docs/guides/onboarding-completion

Send a congratulatory email the moment a user finishes your onboarding checklist, plus a next-step nudge if they don't.

A common SaaS lifecycle pattern: celebrate when a user completes onboarding, and re-engage the users who stall. This recipe sets up both with two [workflows](/docs/workflows).

The core idea: fire an [event](/docs/events) from your app when the user completes onboarding and update an `onboardingCompletedAt` contact property in the same request. The celebration workflow triggers from the event, and the stalled-user workflow filters out contacts with a completion timestamp.

## What you need

* A timestamp [contact property](/docs/contacts/properties) like `onboardingCompletedAt`
* A named [event](/docs/events) for "onboarding completed"
* The [Loops API](/docs/api-reference/intro) or an [integration](/docs/integrations) writing into Loops

## Set up the trigger

Use the **Event received** [workflow trigger](/docs/workflows/triggers) with an event name like `onboardingCompleted`.

Update the contact's `onboardingCompletedAt` property at the same time so your stalled-user workflow can exclude users who already finished.

If you prefer using contact property changes as the trigger, see the **Contact updated** approach used in [the lifecycle emails guide](/docs/guides/lifecycle-emails).

<Steps>
  <Step title="Send the event from your app">
    Call the [Send event endpoint](/docs/api-reference/send-event) when the user finishes the last onboarding step. The API accepts contact properties as top-level fields in the same request:

    <CodeGroup>
      ```json API request theme={"dark"}
      {
        "email": "user@example.com",
        "onboardingCompletedAt": "2026-04-27T17:00:00Z",
        "eventName": "onboardingCompleted",
        "eventProperties": {
          "daysToComplete": 2
        }
      }
      ```

      ```ts SDK theme={"dark"}
      await loops.sendEvent({
        email: "user@example.com",
        eventName: "onboardingCompleted",
        contactProperties: {
          onboardingCompletedAt: new Date().toISOString(),
        },
        eventProperties: { daysToComplete: 2 },
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Create the workflow">
    In the [Workflow builder](/docs/workflows), pick **Event received** and select `onboardingCompleted`.
  </Step>

  <Step title="Send the celebration email">
    Add a **Send email** node. Congratulate them, surface the next high-value feature, and offer a next action.
  </Step>
</Steps>

## Catching users who stall

The other half of this recipe is a separate workflow that fires when a contact is added but does not complete onboarding within a timer window.

<Steps>
  <Step title="Trigger on Contact added">
    Pick **Contact added** as the [trigger](/docs/workflows/triggers).
  </Step>

  <Step title="Add a timer">
    Wait 3 days.
  </Step>

  <Step title="Filter on onboarding state">
    Use an [audience filter](/docs/workflows#audience-filters) to only send if `onboardingCompletedAt` is empty.
  </Step>

  <Step title="Send the nudge">
    Keep it short. Link to the specific step they missed if you track that.
  </Step>
</Steps>

## Measuring success

* Open rate on the celebration email: should be >50% because it is expected
* Click-through on the stall nudge: track with the `email.clicked` [webhook](/docs/webhooks)
* Completion lift: compare `daysToComplete` distributions before and after this workflow ships

## Read more

<CardGroup>
  <Card title="Lifecycle emails overview" icon="arrows-rotate" href="/guides/lifecycle-emails" />

  <Card title="Workflows" icon="arrows-rotate" href="/workflows" />

  <Card title="Events" icon="bolt" href="/events" />

  <Card title="Filters and segments" icon="filter-list" href="/contacts/filters-segments" />
</CardGroup>


# Your first onboarding emails
Source: https://loops.so/docs/guides/onboarding-emails

Some best practices for building your sender reputation with onboarding emails.

We covered the basics of a Sender Reputation in [this guide](/docs/deliverability/sending-reputation) and this guide will cover some best practices for building your sender reputation with onboarding emails.

If you're new to Loops, we recommend taking some specific steps to help warm up your sending reputation and to ensure your emails land in inboxes.

We want to start with an onboarding workflow and transactional emails before we send larger campaigns to a large list. When we send those larger campaigns, we want to first start with a small list and then slowly increase volume while monitoring the results.

## Onboarding welcome workflow

The first step is to set up a welcome email sent to users after they sign up to your application or subscribe to your list. Recipients of these emails are expecting the email and are most likely to engage with it.

For example, new users who sign up could expect a welcome email from you, welcoming them to your platform and explaining some initial steps to get them started.

Here is an example of the email we at Loops send out to new users:

<img alt="Loops' own welcome email" />

<CardGroup>
  <Card href="https://app.loops.so/templates?category=loops" title="Workflow templates" icon="folder-closed">
    Check out our library of useful templates for creating welcome emails.
  </Card>
</CardGroup>

Users receive these emails because they expect them, so they are more likely to open and engage with them, building your sender reputation.

Get started by [creating workflows](/docs/workflows), which are email automations used to send emails after certain triggers, like a new contact joining your audience or after an event happens in your platform.

### Creating a welcome workflow in Loops

<img alt="A simple welcome workflow" />

<Steps>
  <Step title="Create a workflow">
    Go to your [Workflows page](https://app.loops.so/workflows) and click **New**, or select from one of our [ready-made templates](https://app.loops.so/templates).
  </Step>

  <Step title="Set the trigger">
    Set your trigger to **Contact added** if you want the workflow to fire for every new contact. You can also set a filter to only send to contacts with a specific tag or property, or specifically target it to a specific list.

    Select **Event received** if you want to trigger the workflow based on something happening in an external platform.
  </Step>

  <Step title="Add an email">
    Next, write your email by clicking on the **Send email** node, then **Create email** in the inspector panel on the right-hand side.
    Make sure to make your workflow active by clicking **Start** in the inspector panel header on desktop, or in the top nav on mobile.
  </Step>
</Steps>

## Transactional emails

Transactional emails are by definition emails that are sent to users because of an action they took or need to take to use your platform. Since all emails from Loops send from a single domain, we can also use these emails to build our sender reputation. We recommend starting with these essential transactional emails:

* Login verification emails
* Password reset emails
* Account confirmation emails

These types of emails have high engagement rates since users actively request them, which helps build your sender reputation.

<img alt="Password reset email example" />

<CardGroup>
  <Card href="https://app.loops.so/templates?category=transactional" title="Transactional templates" icon="folder-closed">
    Check out our library of useful transactional email templates.
  </Card>
</CardGroup>

## Targeted campaign

Once your welcome workflow has been active and sending for 2 or 3 days, you should see an open rate of around 40% on the first email after the welcome email.

Now you can start to send a campaign to a small list of engaged users. Here are two recommended approaches:

1. Target recent signups using the `createdAt` property to find users added in the last 3 days
2. Target active users by syncing login data to track your most engaged users

For your first campaign content, we recommend:
✅ Product updates or new feature announcements
❌ Avoid generic newsletters, giveaways or promotional content

Important metrics to monitor:

* Campaign open rate should be above 30%
* Welcome workflow open rate should stay above 40%
* If either drops below these thresholds, pause sending and re-evaluate

Loops helps to mitigate deliverability issues by sending campaign emails in batches, waiting to check open rates and then sending further emails in your list depending on the results. We take great care to make sure your emails have the highest deliverability as possible from our side.

### Sending a campaign to active users in Loops

<Steps>
  <Step title="Define and label &#x22;active&#x22; users">
    You can use the default `createdAt` contact property, or you can create custom [contact properties](/docs/contacts/properties#custom-contact-properties) and use the [API](/docs/api-reference/update-contact) or an [integration](/docs/integrations) to update contacts when an event happens in your account (e.g. a log in).

    For example, you could use the name `lastActive` and choose the "Date" field type. Then whenever the user does a major action in your application, update the contact with a new `lastActive` value:

    ```javascript theme={"dark"}
    POST /v1/contacts/update
    {
      "userId": user.id,
      "lastActive": new Date().getTime() // timestamp in milliseconds
    }
    ```
  </Step>

  <Step title="Send the campaign to your segment">
    Once you have created your campaign in Loops, you need to specify the audience based on your chosen date field.

    In the "Audience" tab, choose "Last Active" (or "Created At"), then "After" and then pick the date three days ago from the date field.

    <img alt="Selecting an active audience" />
  </Step>
</Steps>

## Larger campaigns

Now you have warmed up your account, you can start to send out larger campaigns to more contacts.

You may want to import users from another email platform or your application's user database.

We still recommend to not send to everyone on your list. Try to identify segments in your user list (especially if it's in the thousands) that are most likely to open emails from you.

## Read more

<CardGroup>
  <Card title="Improve your inbox placement" icon="inbox" href="/deliverability/improving-inbox-placement" />

  <Card title="Understand email open rates" icon="envelope-open-text" href="/deliverability/understanding-email-open-rates" />

  <Card title="Delivery optimization" icon="chart-line" href="/deliverability/optimization" />

  <Card title="Build your sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Open rates are a vanity metric
Source: https://loops.so/docs/guides/open-rates

Email open rates are unreliable due to privacy changes like Apple Mail Privacy Protection. Learn what to track instead when measuring engagement.

When it comes to email marketing, there are countless metrics that you can track. Some of these metrics may be fun to track and look great on paper (your computer screen) but unfortunately don’t actually lead to improved business results.

These are vanity metrics.

While you might think that vanity metrics help paint a picture of success or improvements to unknowing stakeholders, they are ultimately a distraction.

Facebook likes, Twitter followers, blog pageviews… these are all vanity metrics because they don’t materially impact the success of your business in a tangible way.

But so are email open rates.

Yes, you read that right. The main metric you’ve been tracking (and sharing) to gauge the success or failure of your email marketing campaigns is (mostly) just for show.

## What is an email open rate?

An email open rate is the number of unique opens your email campaign receives. Calculating open rate is simple:

Number of unique email opens / Number of delivered emails x 100 = Open Rate

Delivered emails are important here because hard bounced emails do not count toward this calculation.

<Info>
  Note: a hard bounced email is an email that cannot be delivered due to an unchanging and permanent reason. Unfortunately, there is nothing that you can do to reverse this to force your email through.
</Info>

Some common reasons for a hard bounce are recipient email address doesn’t actually exist, recipient mail server doesn’t exist, and invalid domain name. While a hard bounce will prevent your email from arriving in its intended inbox it is possible that a hard bounce could be caused by something as simple as your recipient having a typo in their email address.

To keep things simple, let’s assume you sent and delivered an email to 100 different users in your campaign. 23 of your users opened this email giving you an open rate of 23%.

23 / 100 = .23 x 100

Easy enough, right?

Up until recently, open rates were a universally loved way to gauge campaign performance.

So what changed?

## Your open rate is (now) a vanity metric

Apple changed — mostly. With a dominating [59.8% email client market share](https://www.litmus.com/blog/email-client-market-share-february-2022/), any change they make is a huge deal.

New privacy initiatives from many of the largest email clients are quietly removing email senders' ability to successfully track open rates.

Apple is leading the charge with their new “Mail Privacy Protection” (first introduced in 2021 with iOS 15) that allows users to opt in to having Apple pre-load their email upon receipt. By doing so, the email’s tracking pixel will immediately trigger.

<Info>
  Note: an email tracking pixel is a 1px by 1px square image that is inserted into an email and is transparent in color and invisible to the recipient. These tracking pixels are what allow marketers to measure open rate, click rate, traffic sources and more.
</Info>

This change artificially inflates open rates as delivered emails will now automatically show as opened whether they truly have or have not been opened by the intended recipient.

These changes to email open tracking are completely out of the sender’s control.

And to top it all off, these changes affect not only users receiving emails on the Apple Mail app itself but also those who use an Apple client in any way ([regardless of email server](https://www.litmus.com/blog/apple-mail-privacy-protection-for-marketers/)). This means that using Gmail, Yahoo, Outlook, etc. on iOS 15 may also contribute to your now-inflated open rate.

What was once a key metric to track and share with key stakeholders should now be viewed with a bit more skepticism.

## What’s next?

It is highly unlikely that these recent privacy changes will be reversed anytime soon (ever).

So what can you do about it?

The answer may surprise you.

We suggest continuing to run your campaigns as you have been.

<Info>
  Continue to send consistent emails (both in frequency and content), create intriguing and accurate subject lines and body content, segment your list so that only the most relevant audience is receiving your emails, and pay close attention to your unsubscribe rate.
</Info>

However, now you should put a greater emphasis on tracking specific product goals in relation to your email campaigns. Have actionable metrics in mind – metrics around business goals such as generating free trials, converting trials to paid, increased usage of your app or inviting team members.

And actually, this should have been the goal all along.

High open rates don’t necessarily lead to increased sales or profits for your business, the end result of those opens is what really makes the difference.

Now that your focus has transitioned to more tangible KPIs that lead to specific product goals being met it will be much easier to accurately measure the success of your email marketing campaigns.

Looking back on it, maybe your email open rate becoming a vanity metric overnight will ultimately be a blessing in disguise.

Take this as an opportunity to refocus your attention on the KPIs that ultimately matter for your business. High open rates may have been impressive in the past but now it’s time to craft and measure your email campaigns against the things that truly move the needle.


# Recipe: Product updates
Source: https://loops.so/docs/guides/product-updates

Our updated, definitive guide for sending product updates.

## Introduction

A product update should be sent once a month with updates about what you shipped recently. This typically includes new features, improvements, and bug fixes.

Things to keep in mind:

* Brevity is key. Users don't want to read a novel.
* If you send valuable content, users will come to expect (and open) it.
* It's okay to send multiple emails in a month if that's your shipping cadence.

<Card title="Email best practices" icon="lightbulb" href="/guides/open-rates">
  Learn more tips for crafting effective emails and improving open rates.
</Card>

## How to craft a product update email:

<Frame>
  <img alt="Example of a product update email sent from Loops" />
</Frame>

1. [Create a new email Campaign](/docs/creating-emails/editor)
2. Add your logo at the top of the email, [save it as a component](/docs/creating-emails/components) if you haven't already.
3. Add a simple subject line, like "The latest updates from Loops"
4. Add a simple intro paragraph, with a link to the full changelog, potentially socials and a highlight of the most important changes along with a call to action to read the full email.
5. Try to limit the number of updates to 2-3, and make sure they are relevant to the content. It's better to have a single or a couple impactful, relevant updates than a long list of updates that are not relevant to the user.
6. In the footer, add a link to the full changelog, socials, and a link to unsubscribe.
7. [Send the email!](/docs/sending-first-email)

<Warning>
  Be careful not to overwhelm your users with too many updates. Focus on the
  most impactful changes.
</Warning>

## Choosing your audience

### For new senders

If you're just starting out, send to all users and maintain a steady cadence over time.

### For established senders

If you have an existing list but haven't sent product updates before:

* Send to active users who have engaged with your product in the last 30 days
* Include users who have created an account in the last 30 days
* If the audience size is less than 5,000 users, send to all users

<Card title="Audience segmentation" icon="users-viewfinder" href="/contacts/filters-segments">
  Learn how to create targeted segments for your product updates.
</Card>

<CardGroup>
  <Card title="Creating emails" icon="envelope" href="/sending-first-email">
    Learn how to create and send your first email in Loops.
  </Card>

  <Card title="Personalizing emails" icon="wand-magic-sparkles" href="/creating-emails/personalizing-emails">
    Add dynamic content to make your product updates more relevant.
  </Card>

  <Card title="Email components" icon="shapes" href="/creating-emails/components">
    Create reusable elements for consistent product update emails.
  </Card>

  <Card title="Types of emails" icon="envelope-circle-check" href="/types-of-emails">
    Explore different types of emails you can send with Loops.
  </Card>
</CardGroup>


# Retrigger a Workflow
Source: https://loops.so/docs/guides/retroactively-trigger-workflow

Backfill contacts into a workflow.

<Warning>
  This is a temporary workaround while we build native backfill. The current
  flow is clunky and will get much better. Please still let us know you want
  native backfill so we can prioritize it.
</Warning>

## When to use this

* You have an existing workflow that should have run for some contacts but didn’t.
* You’re comfortable exporting and re-importing a contact list.

## What you’ll do (high level)

1. Duplicate the original workflow.
2. Create a one-off contact property (e.g., `backfill_welcome_sep2025`).
3. Change the duplicate workflow’s trigger to “Contact updated” based on that property.
4. Start the duplicated workflow.
5. Export the intended audience.
6. Add the property to the CSV for those contacts.
7. Re-import the CSV to update contacts and trigger the workflow.

## Step-by-step

<Steps>
  <Step title="Duplicate the workflow">
    Open the workflow you want to retroactively trigger.

    Use **Duplicate** to create a copy. Rename it clearly (e.g., “Welcome Backfill (Sep 2025)”).
  </Step>

  <Step title="Create a one-off contact property">
    Add a new contact property to use exclusively for this backfill.

    **Example:**
    Boolean: `backfill_welcome_sep2025` (true/false)

    Use a name that avoids collisions with any existing logic.
  </Step>

  <Step title="Change the duplicate workflow’s trigger">
    Select “Contact updated” as the trigger.

    Set the criteria to:

    * was any
    * is equal to `true`

    <Tip>
      For one-off backfills, set Trigger frequency to <strong>One time</strong> so a
      contact only enters once.
    </Tip>
  </Step>

  <Step title="Export the intended audience from the original workflow">
    From the main audience table, [export the contacts](/docs/contacts/export-contacts) who should be backfilled.
  </Step>

  <Step title="Prepare the CSV for re-import">
    Open the CSV and add a new column with the property key you created.

    Populate that column with `true` for every row you want to trigger.

    Ensure the CSV still includes the primary identifier (either `email` or `userId`).
  </Step>

  <Step title="Re-import to update contacts and trigger">
    Import the CSV from the **Import** button on the [Audience page](https://app.loops.so/audience).

    Confirm the column mapping so your backfill property maps 1:1 to the new contact property.

    Set the CSV import to [trigger workflows](/docs/add-users/csv-upload#trigger-workflows-via-csv) using the **Trigger workflows** toggle.

    Start the import.

    Your backfilled workflow will begin triggering immediately.
  </Step>
</Steps>

<Warning>
  After the backfill completes, consider removing or resetting the temporary
  contact property to avoid accidental future triggers.
</Warning>


# Recipe: Scheduled digest email
Source: https://loops.so/docs/guides/scheduled-digest-email

How to send a daily, weekly or monthly email with a summary of what's happened in your app.

This email type may also be referred to as a "rollup" email or a "summary" email. The idea is to send a single email that summarizes what's happened in your app over a period of time.

These kinds of digest emails are a great way to keep your users engaged with your app.

The best way to do it today is a workflow with an [event trigger](/docs/workflows/triggers) set to fire “every time” with an event payload containing the updated property you’d like to send.

Then at the end of the day, week or month you can trigger a digest email with a summary of the events that happened.

## Create the workflow and event

Go to the [Workflows](https://app.loops.so/workflows) page and create a new workflow.

Choose the **Event is fired** trigger. You will enter the workflow editor.

Click on the **Event received** node and type the name of your event in the inspector panel on the right-hand side. You can reuse an existing event or create a new one from this input.

For example, you can use a name like `sendDigest` and then click **+ Create new event**.

<img alt="Creating a trigger node" />

Next, click **Edit event properties** in the inspector panel to add [properties](/docs/events/properties) to your event. Properties are extra pieces of data that you can attach to each event. This data is then made available in every email you send.

In the event editor overlay, click **+ Add event property** and add any properties you want to include in your digest email.

<img alt="Adding event properties" />

Make sure the **Trigger frequency** dropdown in the inspector panel has **Every time** selected, so that each event triggers an email.

## Create your email

The next step is to create the email you send to each contact.

Click on the **Send email** node and then **Create email**. This will open the email editor, where you can [create your email](/docs/creating-emails/editor).

When you want to add the event properties you created in the previous step, click the `{}` icon above the editor (1) (or type `{` in the editor) to open the dynamic content menu, then select the event property you want to insert (2).

<img alt="Adding event properties in the editor" />

See [personalizing emails](/docs/creating-emails/personalizing-emails) for more on inserting available event properties into your email.

Make sure to [add fallback values](/docs/creating-emails/personalizing-emails#fallback-values) for every property; if an event doesn't have a value for a property in your email, the email will not send. Fallback values make sure that emails are sent to every contact.

When you've finished creating your email, click **Start** in the inspector panel header on desktop, or in the top nav on mobile. This will make your workflow active and you can start triggering it by sending events.

## Trigger events

To send events to Loops you can use an [integration](/docs/integrations), an [SDK](/docs/sdks) or [our API](/docs/api-reference/intro).

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

With the API, it's just a case of making a request to the [Send event endpoint](/docs/api-reference/send-event) containing the contact's details, the event name and your event properties.

```json theme={"dark"}
{
  "eventName": "sendDigest",
  "email": "test@example.com",
  "eventProperties": {
    ...
  }
}
```

## Learn more

<CardGroup>
  <Card title="Workflows" icon="arrows-rotate" href="/workflows">
    Learn about creating email workflows.
  </Card>

  <Card title="Workflow Triggers" icon="circle-play" href="/workflows/triggers">
    Read more about triggering emails with events.
  </Card>

  <Card title="Personalizing emails" icon="crystal-ball" href="/creating-emails/personalizing-emails">
    Learn how to add dynamic data to your emails.
  </Card>

  <Card title="API Reference" icon="rectangle-terminal" href="/api-reference/intro">
    Find out how to send events using our API.
  </Card>
</CardGroup>


# How to resend a campaign to new subscribers
Source: https://loops.so/docs/guides/send-again

A quick guide for sending a campaign to subscribers who signed up since you sent it out.

If you're regularly adding new subscribers via [a form](/docs/forms/simple-form), [integration](/docs/integrations) or [the API](/docs/api-reference/intro), it's likely that you will want to send a campaign to new contacts who signed up after you sent it initially.

In Loops, you can do this in two easy steps: duplicating the campaign and filtering the audience.

## Duplicate the campaign

The first step is to duplicate the campaign so that you can send it out again.

To do this, go to the Campaigns page and click on the `•••` menu icon, then select **Duplicate**.

<img alt="Duplicating emails" />

## Send to your new contacts

When you're ready to send the email, it's important that it only gets sent to contacts that haven't been sent it already.

To do this, on the **Audience** page of the sending flow, select "Not Sent" from the first dropdown and then the *original* campaign from the third dropdown.

<img alt="Filter audience by sent campaign" />

This will update the total contacts and the table below, which will show contacts that were not sent the original campaign.

Check that everything looks OK and continue sending the email.


# Transactional vs marketing email
Source: https://loops.so/docs/guides/transactional-vs-marketing-email

When to use transactional email, when to use marketing email, and how Loops keeps the two separate so neither hurts the other.

Misclassifying an email (sending marketing content through the transactional API, or a slow transactional message through a marketing path) hurts both deliverability and user expectations. This guide covers when each path is appropriate, what Loops does to keep them separate, and how to pick when you are not sure.

For an overview of the three sending types in Loops (Campaigns, Workflows, Transactional), see [Types of emails](/docs/types-of-emails).

## Quick answer

| Send it as...                          | When the email is...                                                                                          |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [Transactional](/docs/transactional)        | Triggered by a specific user action, expected by that user, and contains only content relevant to that action |
| [Workflow](/docs/workflows)                 | Triggered by an event or property change, part of an ongoing lifecycle sequence                               |
| [Campaign](/docs/types-of-emails#campaigns) | A one-off send to a segment of your Audience                                                                  |

Workflows and Campaigns are both marketing email. Transactional is its own category.

## What counts as transactional

[Transactional email](/docs/transactional#transactional-vs-marketing-emails) is 1-to-1 and triggered by the user. Clear examples:

* Password resets and magic links
* Email verification and OTP codes
* Receipts, invoices, and payment confirmations
* Shipping updates
* Account creation and deletion confirmations
* Security alerts ("new login from Chrome on macOS")
* Scheduled report exports the user requested

Less clear examples, with the typical call:

| Email                                                                       | Typical classification                                                                                                                               |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Welcome email right after signup                                            | Marketing. Send as a [Workflow](/docs/workflows) on **Contact added**. The user signed up, but the email is relationship-building, not action-completing. |
| Weekly [scheduled digest](/docs/guides/scheduled-digest-email) of their activity | Marketing, even though it is personalized                                                                                                            |
| "Your trial ends in 3 days"                                                 | Marketing, it is promoting renewal                                                                                                                   |
| "Your trial ended, data will be deleted in 30 days"                         | Usually transactional, because it reflects account state and deletion timing the user needs                                                          |
| [Team invitation](/docs/account/team-members) email                              | Usually transactional, the invitee took no action yet but it completes the inviter's action                                                          |
| Re-engagement email                                                         | Marketing                                                                                                                                            |

When in doubt: if the user would reasonably be surprised to receive it, or if it contains any promotional content, send it as marketing. For individual cases you are unsure about, email [help@loops.so](mailto:help@loops.so).

## Why the distinction matters

### Sender reputation

Transactional email has high engagement by default because the recipient is actively waiting for it. That engagement boosts your [sender reputation](/docs/deliverability/sending-reputation). Marketing email has lower engagement, and sending marketing content through a transactional path mixes the two signals and can hurt deliverability for the messages that matter.

Loops sends transactional and marketing email from your verified sending domain and a small shared pool of Loops IPs by default, so the reputation signals are connected. This is usually helpful because high transactional engagement can support overall deliverability. It also means marketing content sent through a transactional path can weaken the same reputation profile that your operational messages depend on.

### Legal and compliance

Marketing email must include an unsubscribe link and honor opt-outs. Transactional email is exempt because it is operationally required. Loops enforces this automatically:

* Campaigns and Workflow emails include an unsubscribe link in the Loops footer, or you add `{unsubscribe_link}` when [uploading custom emails](/docs/creating-emails/uploading-custom-email#add-an-unsubscribe-url).
* Transactional emails do not include an unsubscribe link.

### Unsubscribe vs suppression

These are two different states in Loops, and the distinction matters when deciding classification:

| State                                                                                  | What it means                                    | Transactional still sends? | Marketing still sends? |
| -------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------- | ---------------------- |
| **Unsubscribed** ([`subscribed` property](/docs/contacts/properties#subscribed) is `false`) | The contact opted out of marketing               | Yes                        | No                     |
| **Suppressed** ([suppression](/docs/contacts/suppression))                                  | Hard bounce or complaint, Loops blocks all sends | No                         | No                     |

So "transactional bypasses unsubscribes" is true. "Transactional bypasses suppression" is not, see [contact suppression](/docs/contacts/suppression).

### Tracking

By default, Loops does not track opens or clicks on transactional email. This is deliberate, see [the note in the transactional guide](/docs/transactional#metrics). Marketing email tracks both.

This means `email.opened` and `email.clicked` [webhook events](/docs/webhooks) do not fire for transactional sends.

## How Loops keeps them separate

| Behavior                                    | Campaigns and Workflows                              | Transactional                                                                |
| ------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
| Unsubscribe link                            | Required (auto-added or `{unsubscribe_link}` tag)    | Not added                                                                    |
| Open and click tracking                     | On                                                   | Off                                                                          |
| Added to Audience                           | Yes, on signup or contact creation                   | Only with `"addToAudience": true`                                            |
| Triggers "Contact added" workflow           | Yes                                                  | No                                                                           |
| Honors `subscribed: false`                  | Yes, does not send                                   | No, still sends                                                              |
| Honors [suppression](/docs/contacts/suppression) | Yes                                                  | Yes                                                                          |
| Sent via                                    | Editor, [workflows](/docs/workflows), scheduled campaigns | [Send transactional email API](/docs/api-reference/send-transactional-email) only |

Full behavior reference: [Transactional email overview](/docs/transactional).

## Common mistakes

### Sending a "welcome" email as transactional

The user technically took an action (signing up), but a welcome email is usually promotional or onboarding content. Send it as a [workflow](/docs/workflows) with a "Contact added" trigger so it respects unsubscribes and contributes to your marketing engagement signal.

For examples of what to include, see [Your first onboarding emails](/docs/guides/onboarding-emails).

### Sending marketing content through the transactional API

Mixing product announcements or newsletters into transactional sends risks complaints and spam flags. Email service providers (ESPs) watch for this. The fix is to send marketing content as a [Campaign](/docs/types-of-emails#campaigns) or [Workflow](/docs/workflows) instead.

### Forgetting transactional still needs a [verified sending domain](/docs/sending-domain)

Transactional uses the same domain setup as marketing. If DNS is not right, nothing sends. See [Setting up your domain](/docs/sending-domain).

### Using marketing-style "From" names on transactional

Transactional should come from a From name the user recognizes from the app (for example, `Loops <noreply@mail.loops.so>`). Overly branded or promotional From names on receipts and security alerts confuse users and hurt engagement. See [sending settings](/docs/creating-emails/sending-settings).

## Read more

<CardGroup>
  <Card title="Types of emails" icon="envelopes-bulk" href="/types-of-emails" />

  <Card title="Transactional email" icon="envelope" href="/transactional" />

  <Card title="Workflows" icon="arrows-rotate" href="/workflows" />

  <Card title="Sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>


# Recipe: Upgrade upsell workflow
Source: https://loops.so/docs/guides/upgrade-upsell

Trigger an upsell sequence when a free-plan user hits a usage threshold that a paid plan would solve.

Generic "upgrade now" emails get ignored. Specific ones ("you sent 950 of your 1000 monthly emails") convert. This recipe fires an upsell [Workflow](/docs/workflows) only when a user hits a usage threshold that a paid plan would remove, personalized to what they actually did.

## What you need

* An event from your app when the user approaches a plan limit
* A way to pass the usage context as [event properties](/docs/events/properties)
* A [Stripe integration](/docs/integrations/stripe) is handy but not required

## Send the signal from your app

When a free-plan user hits 80% of any plan limit, fire an event via the [Send event endpoint](/docs/api-reference/send-event):

<CodeGroup>
  ```js REST theme={"dark"}
  await fetch("https://app.loops.so/api/v1/events/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOOPS_API_KEY}`,
    },
    body: JSON.stringify({
      email: user.email,
      eventName: "approachingPlanLimit",
      planName: "Free",
      eventProperties: {
        usageCurrent: 950,
        usageMax: 1000,
        usagePercent: 95,
      },
    }),
  });
  ```

  ```ts SDK theme={"dark"}
  await loops.sendEvent({
    email: user.email,
    eventName: "approachingPlanLimit",
    contactProperties: {
      planName: "Free",
    },
    eventProperties: {
      usageCurrent: 950,
      usageMax: 1000,
      usagePercent: 95,
    },
  });
  ```
</CodeGroup>

See [Events](/docs/events) and [Event properties](/docs/events/properties) for the full API.

Note: this `approachingPlanLimit` event is specific to your app's usage tracking. The [Stripe integration](/docs/integrations/stripe) covers subscription, invoice, checkout, quote, and dispute events (see the full list in the integration doc), but usage-limit events still need to come from your own backend.

## Build the workflow

<Steps>
  <Step title="Trigger on Event received">
    In the [Workflow builder](/docs/workflows), pick **Event received** and select
    `approachingPlanLimit`.
  </Step>

  <Step title="Filter on plan">
    Add an [audience filter](/docs/workflows#audience-filters) to your workflow so you only send to
    free-plan users: `planName = Free`. Set the filter scope to **All following nodes**.
  </Step>

  <Step title="Personalize the email">
    Use [event properties](/docs/events/properties) directly in the email. In the
    editor, click the `{}` icon above the editor or type `{` and then the event property name:

    ```
    You have sent {usageCurrent} of {usageMax}
    emails this month ({usagePercent}%). Upgrade to keep going.
    ```
  </Step>

  <Step title="Send a follow-up email">
    Add a follow-up email to your workflow that is sent 3 days after the initial email.
    Your initial "All following nodes" audience filter will still be applied to the follow-up email.
  </Step>
</Steps>

## Why event-triggered beats scheduled

Scheduled "here is our pro plan" campaigns hit people who aren't close to a limit and who did nothing to prompt the message. Event triggers mean every send goes to a user with a concrete reason to upgrade right now.

<Tip>
  If your pricing has multiple limits (users, storage, emails, integrations),
  fire a separate event type per limit, triggering a separate workflow for each.
</Tip>

## Measuring success

* Open rate: should be higher than a generic upsell because the subject can cite the specific resource
* Upgrade conversion: track via your existing billing system, not in Loops
* Unsubscribe rate: watch this closely, over-sending upsells is a fast way to lose users

## Read more

<CardGroup>
  <Card title="Events" icon="bolt" href="/events" />

  <Card title="Event properties" icon="key" href="/events/properties" />

  <Card title="Stripe integration" icon="credit-card" href="/integrations/stripe" />

  <Card title="Branching workflows" icon="code-branch" href="/workflows/branching" />
</CardGroup>


# What is BIMI?
Source: https://loops.so/docs/guides/what-is-bimi

Learn what BIMI is, how it works, and how to set up a BIMI record for your sending domain.

BIMI (Brand Indicators for Message Identification) is an email standard that lets supporting inbox providers display your verified brand logo next to authenticated emails.

## What is BIMI?

BIMI, or Brand Indicators for Message Identification, is a relatively new email standard that allows you to add your brand’s logo to your authenticated emails.

Adopting BIMI will allow your brand’s logo to appear next to your emails in your recipient’s inbox without manually needing to add and maintain it on a provider by provider level. As long as the email client supports BIMI, your logo will appear.

## Which email providers support BIMI?

A growing number of email providers support BIMI. The current list of supported providers includes:

* Gmail
* Google Workspace
* Apple Mail ([macOS Ventura 13, iOS 16, and iPadOS 16, or later](https://developer.apple.com/support/bimi/))
* AOL
* Netscape
* Fastmail
* Yahoo (Yahoo Japan is not currently supported but is under consideration for future adoption)
* Pobox

Here is the full breakdown of who does and doesn’t support BIMI as of right now:

<img alt="Infographic showing BIMI support by mailbox providers" />

<small>Image via [BIMI Group](https://bimigroup.org/bimi-infographic/)</small>

## How does BIMI actually work?

Setting up BIMI will take a bit more work than simply uploading your brand’s logo and hitting save.

Technically speaking, BIMI is configured with a DNS TXT record (your “BIMI record”) that points inbox providers to your hosted logo (and optionally your Verified Mark Certificate).

At a broad level, after you send an email, your recipient’s email provider verifies your email authentication (SPF, DKIM, and DMARC). If those checks pass, the provider can look up your BIMI record to fetch and display your logo next to the message.

To start, the sender (you) will need to ensure that you are DMARC compliant. DMARC (Domain-based Message Authentication, Reporting, & Conformance) is an [email authentication policy and reporting protocol](https://bimigroup.org/faqs-for-senders-esps/) that defends against unauthorized use of domains.

Basically, DMARC helps protect your brand by detecting emails that aren’t coming from your domain – preventing spoofing and phishing attempts.

The email provider will also run through the Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) protocols to ensure that the sender’s email address was sent from the correct domain.

Next, you will need to create the logo that will actually be used. The current recommendation is a [square SVG file](https://bimigroup.org/creating-bimi-svg-logo-files/).

The naming convention of this file should be:

`https://yourservername.com/logo.svg`

Next is an optional but recommended step. To fully embrace BIMI, your brand should acquire a VMC, or a Verified Mark Certificate. This will help validate the true ownership of the logo being used. More on this in the section below.

The full implementation guide from BIMI can be located [here](https://bimigroup.org/implementation-guide/).

## BIMI requirements (high level)

Before BIMI can work, inbox providers generally require that you have strong email authentication in place:

* SPF configured for your sending domain
* DKIM signing enabled
* DMARC set up with an enforcement policy (often `quarantine` or `reject`)

If you’re setting up sending in Loops, start with our [sending domain guide](/docs/sending-domain) and then review the deliverability docs (like [sending reputation](/docs/deliverability/sending-reputation)).

## How to set up BIMI (step-by-step)

1. Make sure SPF and DKIM are set up and passing for your sending domain.
2. Configure DMARC and move to an enforcement policy once you’re ready.
3. Create a BIMI-compatible SVG logo and host it at a public URL.
4. (Optional) Purchase a VMC if you want the strongest verification.
5. Add the BIMI TXT record to DNS (usually at `default._bimi.yourdomain.com`) and reference your hosted logo (and VMC if you have one). For example:

   ```txt theme={"dark"}
   v=BIMI1; l=https://yourdomain.com/logo.svg; a=https://yourdomain.com/vmc.pem
   ```

   Notes:

   * `l=` is the URL to your BIMI SVG logo.
   * `a=` is optional and points to your VMC (Verified Mark Certificate).
6. Validate your setup using the [BIMI inspector](https://bimigroup.org/bimi-generator/).

## How much does BIMI cost?

Along with taking some technical chops to get BIMI fully set up, it’s also not free.

Getting the Verified Mark Certificate mentioned above currently [costs \$1,500](https://www.digicert.com/tls-ssl/verified-mark-certificates).

On top of this cost, to qualify for the certificate your brand logo also needs to be a registered trademark, which will also come with additional costs and possible legal fees.

## Checking your BIMI record

Now that you’ve gone through the work of setting up your BIMI records, it’s time to confirm that everything is working as expected.

The BIMI group provides a [BIMI inspector](https://bimigroup.org/bimi-generator/) where you can enter your domain to ensure everything is set.

## Implement BIMI, build trust

As you can see, fully embracing BIMI is a lengthy and potentially expensive process that may test your patience.

However, leaning into this new email standard positions your brand to gain your reader’s trust while limiting the manual labor and upkeep on your end to ensure that your brand’s logo is always at the forefront of their inboxes.

As the inbox becomes a more and more competitive landscape with each passing day, anything that you can do to stand out should be done.

BIMI just might give your brand the edge it needs to capture those sought after eyeballs.


# What is DNS?
Source: https://loops.so/docs/guides/what-is-dns

A beginner’s guide to DNS and the DNS records (MX, TXT, CNAME) used for sending email.

Have you ever wondered how the internet seems to know precisely where to go when you type a website's name into your browser's address bar? How does your computer know to show you the right page when you type in `loops.so`? The answer to these questions lies in the Domain Name System or DNS.

DNS is an essential part of the Internet's inner workings, serving as the Internet's equivalent of your iPhone’s address book. But what exactly is DNS, and why is it so important?

To understand DNS, we first need to understand how the Internet works. Let’s dive in!

## What is DNS?

Every device connected to the Internet, including computers, phones, and servers, has a unique IP address.

An IP address is a string of numbers separated by periods that identifies each computer using the Internet Protocol to communicate over a network. For example, an IP address might look like this: "192.168.0.1".

However, humans generally find it much easier to remember names rather than strings of numbers. This is where the **Domain Name System (DNS)** comes in. The DNS translates the domain names that we humans easily understand and remember into the IP addresses that computers use to identify each other on the network!

For instance, when you type in `loops.so` into your web browser, your computer sends a request to a DNS server asking for the IP address associated with that domain name.

The DNS server responds with the IP address, and your computer then sends a request to that IP address to fetch and display the website.

The process of converting domain names into IP addresses is known as **DNS resolution**, and it usually takes only milliseconds. DNS servers are strategically located around the world and work together to ensure that these requests are processed quickly and accurately.

## Why is DNS important

Now, why is DNS crucial? Firstly, it makes the internet user-friendly.

Without DNS, we would have to memorize complex IP addresses for each website we wanted to visit. Secondly, it ensures the smooth operation of the internet. Every time you send an email, browse a website, or use an app, DNS is working behind the scenes to route your request to the right destination.

And if that wasn’t enough, DNS also provides a level of security. DNS servers can filter and block access to certain websites that might be harmful or inappropriate, providing an essential layer of protection for internet users. It helps you verify the authenticity of a sender via email as we’ll cover in the next section.

## DNS and email

DNS is vital for the functioning of email.

### Common DNS record types for email

When you set up email sending (including with Loops), you’ll typically work with a few common DNS record types:

* **MX records**: Tell the world which servers receive email for your domain.
* **TXT records**: Used for many kinds of verification, including SPF and DMARC.
* **CNAME records**: Used for domain verification and routing/aliasing in some setups.
* **BIMI records**: A specialized TXT record used by supporting inbox providers to display your verified logo. Learn more in [What is BIMI?](/docs/guides/what-is-bimi).

### Sending email

When you send an email, your email client needs to know where to send it.

Let's say you're sending an email to `someone@gmail[.]com`.

The client doesn't inherently know where "gmail.com" is, just like your web browser wouldn't know where to go if you entered that into the address bar. DNS steps in to resolve "gmail.com" into an IP address that represents the actual server your email needs to reach.

### Routing email to the right location

DNS is used for email routing, particularly through a type of DNS record called the MX (Mail Exchanger) record.

An MX record is a type of record that specifies a mail server responsible for accepting email messages on behalf of a recipient's domain.

A priority value (a number like 10 is typical) is used to prioritize mail delivery if multiple mail servers are available. Without the MX records, your email wouldn't know which server to go to.

### Security

Most importantly, DNS plays a critical role in email security.

For instance, Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) are two methods used to prevent email spoofing, a technique used in phishing and spam campaigns where the sender masquerades as another by forging the header data.

SPF and DKIM utilize DNS to hold text records that a recipient's server can check to verify the sender's identity.

If you’re configuring your sending domain in Loops, start with our [sending domain setup guide](/docs/sending-domain) to add the required DNS records.

In essence, DNS is a cornerstone for email operations and security. It helps your email find its way to the correct server, ensures the recipient's server can accept the mail, and provides mechanisms to verify the sender's identity to combat fraudulent activities. Without DNS, our email system would be far less reliable and secure.

## Send better email with Loops

In summary, the Domain Name System, or DNS, is a critical component of both the internet and email as a whole, transforming the web from a complicated network of numeric addresses into an accessible and secure environment for users worldwide.

It's the silent hero that allows us to navigate the digital world with ease, translating memorable website names into computer-friendly IP addresses.

So, the next time you browse the internet or send that time-sensitive email, take a moment to appreciate the extraordinary system that makes it all possible: **DNS**.


# Integrations
Source: https://loops.so/docs/integrations

Browse Loops integrations to sync contacts and trigger emails from around the internet.

Loops integrations let you connect Loops to your product and your workflow tools so you can sync contacts, trigger workflows, and send transactional email.

If you’re looking for the fastest way to get started, try [Zapier](/docs/integrations/zapier) or [Make](/docs/integrations/make). For custom setups, use [incoming webhooks](/docs/integrations/incoming-webhooks) or the [Loops API](/docs/api-reference/intro).

## Featured integrations

<CardGroup>
  <Card title="Stripe" icon="stripe-s" href="/integrations/stripe">
    Sync customers to your audience and send automated emails.
  </Card>

  <Card title="Supabase" icon={SupabaseIcon()} href="/smtp/supabase">
    Send Supabase authentication emails with Loops.
  </Card>
</CardGroup>

## Manage contacts

<CardGroup>
  <Card
    title="Attio"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" fill="none">
    <path
      fill="#FF4A00"
      d="m29.754 22.362-2.512-4.02s-.009-.017-.015-.024l-.198-.316a2.03 2.03 0 0 0-1.726-.96l-4.046-.014-.282.453-4.835 7.736-.267.428L17.9 28.88c.374.602 1.02.961 1.732.961h5.67c.699 0 1.36-.368 1.73-.959l.2-.32s.008-.008.01-.012l2.515-4.025a2.045 2.045 0 0 0 0-2.164h-.002Zm-.766 1.683-2.516 4.025c-.01.02-.024.034-.035.05a.34.34 0 0 1-.544-.05l-2.515-4.027a1.116 1.116 0 0 1-.13-.29 1.127 1.127 0 0 1 .127-.908l2.512-4.02.006-.01c.06-.09.135-.131.2-.144.026-.008.049-.01.067-.013h.028c.058 0 .202.018.292.164l2.511 4.02c.23.366.23.837 0 1.203h-.003ZM22.322 12.636a2.053 2.053 0 0 0 0-2.164l-2.512-4.02-.21-.338a2.031 2.031 0 0 0-1.732-.959h-5.67c-.707 0-1.354.36-1.731.96L.314 22.366a2.03 2.03 0 0 0-.002 2.162l2.723 4.359a2.026 2.026 0 0 0 1.73.959h5.67c.712 0 1.358-.36 1.732-.96l.208-.33v-.004l.003-.007 2.024-3.237 5.999-9.6 1.917-3.07.004-.001Zm-.593-1.082c0 .207-.058.416-.175.601l-9.946 15.918a.34.34 0 0 1-.291.16.342.342 0 0 1-.292-.16l-2.513-4.027a1.141 1.141 0 0 1 0-1.202l9.945-15.913a.339.339 0 0 1 .292-.163c.058 0 .202.017.293.164l2.512 4.02c.117.185.175.394.175.602Z"
    ></path>
  </svg>
}
    href="/integrations/auto-bcc"
  >
    Sync outgoing mail and contacts to Attio.
  </Card>

  <Card
    title="Bubble"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17">
    <g fill="none" fillRule="nonzero" transform="translate(.5 .111)">
      <path
        fill="#FF4A00"
        d="M9.361 4.277c-1.474 0-2.928.633-4.037 1.877V.016H3.128v10.336a6.077 6.077 0 0 0 12.151 0c0-3.354-2.563-6.075-5.918-6.075Zm-.157 9.81a3.735 3.735 0 1 1 0-7.47 3.735 3.735 0 0 1 0 7.47Z"
      />
      <circle cx="1.53" cy="14.925" r="1.503" fill="#FF4A00" />
    </g>
  </svg>
}
    href="/integrations/bubble"
  >
    Manage contacts and send emails from your Bubble app.
  </Card>

  <Card title="Clay" href="/integrations/clay" icon="rainbow">
    Sync users to and from your audience.
  </Card>

  <Card
    title="Clerk"
    icon={
  <svg viewBox="0 0 18 18" fill="none" aria-hidden="true">
    <ellipse
      cx="8.99999"
      cy="9"
      rx="2.81249"
      ry="2.8125"
      fill="#FF4A00"
    ></ellipse>
    <path
      fill="#FF4A00"
      d="M14.0674 15.6591C14.3067 15.8984 14.2827 16.2945 14.0015 16.4829C12.571 17.4411 10.8504 17.9999 8.9993 17.9999C7.14818 17.9999 5.42758 17.4411 3.99704 16.4829C3.71589 16.2945 3.69186 15.8984 3.93115 15.6591L5.98648 13.6037C6.17225 13.418 6.46042 13.3886 6.69424 13.5084C7.3856 13.8626 8.16911 14.0624 8.9993 14.0624C9.82948 14.0624 10.613 13.8626 11.3044 13.5084C11.5382 13.3886 11.8263 13.418 12.0121 13.6037L14.0674 15.6591Z"
    ></path>
    <path
      fill="#FF4A00"
      d="M14.0022 1.51706C14.2834 1.70539 14.3074 2.10155 14.0681 2.34084L12.0128 4.39619C11.827 4.58195 11.5388 4.61129 11.305 4.49151C10.6136 4.13733 9.83014 3.9375 8.99996 3.9375C6.20403 3.9375 3.93748 6.20406 3.93748 9C3.93748 9.83018 4.13731 10.6137 4.49149 11.3051C4.61127 11.5389 4.58193 11.827 4.39617 12.0128L2.34083 14.0682C2.10154 14.3074 1.70538 14.2834 1.51705 14.0023C0.558857 12.5717 0 10.8511 0 9C0 4.02944 4.02942 0 8.99996 0C10.8511 0 12.5717 0.55886 14.0022 1.51706Z"
      fillOpacity="0.5"
    ></path>
  </svg>
}
    href="/integrations/clerk"
  >
    Sync users to your audience and send automated emails.
  </Card>

  <Card
    title="Fivetran"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
    <g clip-path="url(https://mintlify.s3.us-west-1.amazonaws.com/loops/#clip0_6726_139524)">
      <path d="M15.7844 15.8802H18.9735C19.2275 15.8855 19.4392 15.6831 19.4446 15.429C19.4459 15.3642 19.4339 15.2993 19.4088 15.2398L13.9213 0.304356C13.8552 0.121749 13.6818 -0.00131267 13.4873 1.05676e-05H10.2864C10.0323 -0.00131267 9.82456 0.20379 9.82324 0.459175C9.82324 0.514751 9.8325 0.569005 9.85238 0.620611L15.3385 15.5759C15.406 15.7625 15.586 15.8855 15.7844 15.8802Z" fill="#FF4A00"/>
      <path d="M15.2091 31.7578H18.3981C18.6522 31.7591 18.8599 31.554 18.8613 31.2986C18.8613 31.243 18.852 31.1888 18.8321 31.1372L7.51712 0.304352C7.45095 0.121745 7.27763 6.36688e-06 7.08175 6.36688e-06H3.89673C3.64267 -0.00131687 3.43492 0.203786 3.43359 0.459171C3.43359 0.514747 3.44286 0.569 3.46271 0.620606L14.7751 31.4561C14.8426 31.6374 15.0159 31.7578 15.2091 31.7578Z" fill="#FF4A00"/>
      <path d="M8.93027 31.7577H12.1193C12.3734 31.7696 12.589 31.5724 12.601 31.3183C12.6036 31.2561 12.5943 31.1953 12.5731 31.137L7.06713 16.1818C7.00095 15.9992 6.82764 15.8761 6.63313 15.8775H3.42823C3.17417 15.8841 2.97303 16.0958 2.97965 16.3499C2.98098 16.4015 2.99156 16.4531 3.01009 16.502L8.49627 31.4573C8.56375 31.6372 8.73707 31.7577 8.93027 31.7577Z" fill="#FF4A00"/>
      <path d="M16.17 0.624583L18.7424 7.62057C18.8086 7.80314 18.9819 7.92623 19.1765 7.92491H22.3655C22.6195 7.92623 22.8273 7.72114 22.8286 7.46571C22.8286 7.41017 22.8194 7.35589 22.7995 7.30429L20.2271 0.304356C20.161 0.121749 19.9876 -0.00131267 19.7931 1.05676e-05H16.6041C16.35 -0.00131267 16.141 0.202466 16.1396 0.457852C16.1396 0.514751 16.1489 0.571651 16.17 0.624583Z" fill="white"/>
      <path d="M3.04742 31.7588H6.23646C6.49051 31.7601 6.69823 31.555 6.6996 31.2996C6.6996 31.244 6.69029 31.1898 6.67046 31.1382L4.08353 24.1237C4.01736 23.9411 3.84402 23.818 3.6495 23.8193H0.464458C0.210395 23.818 0.00132324 24.0218 0 24.2772C0 24.3341 0.00926269 24.391 0.0304346 24.4439L2.60282 31.4584C2.6703 31.6424 2.85026 31.7641 3.04742 31.7588Z" fill="#FF4A00"/>
    </g>
    <defs>
      <clipPath id="clip0_6726_139524">
      <rect width="114.286" height="32" fill="white"/>
      </clipPath>
    </defs>
  </svg>
}
    href="/integrations/fivetran"
  >
    Add contacts and contact properties from Fivetran (previously Census).
  </Card>

  <Card
    title="Framer"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
    width="24"
    height="24"
    viewBox="0 0 98 148"
  >
    <path
      d="M 93.852 94.49 C 95.387 96.036 94.292 98.667 92.113 98.667 L 51.45 98.667 C 50.097 98.667 49 99.764 49 101.117 L 49 142.057 C 49 144.243 46.353 145.335 44.812 143.783 L 0.949 99.622 C 0.341 99.01 0 98.183 0 97.32 L 0 51.783 C 0 50.43 1.097 49.333 2.45 49.333 L 49 49.333 Z"
      fill="#FF4A00"
    ></path>
    <path
      d="M 49 49.333 L 4.148 4.177 C 2.613 2.631 3.708 0 5.887 0 L 95.55 0 C 96.903 0 98 1.097 98 2.45 L 98 46.883 C 98 48.236 96.903 49.333 95.55 49.333 Z"
      fill="#FF4A00"
    ></path>
  </svg>
}
    href="/integrations/framer"
  >
    Add a Loops signup form to your Framer site.
  </Card>

  <Card title="HubSpot" icon="hubspot" href="/integrations/auto-bcc">
    Sync outgoing mail and contacts to HubSpot.
  </Card>

  <Card
    title="Integrately"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 512 512">
    <path
      fill="#FF4A00"
      fillRule="evenodd"
      d="M256.379 20.676c-129.891 0-235.703 105.401-235.703 235.025 0 128.924 106.059 235.024 235.703 235.024 128.924 0 234.992-105.854 234.992-235.024 0-128.939-106.073-235.025-234.992-235.025ZM5 255.701C5 117.371 117.878 5 256.379 5c137.578 0 250.668 113.106 250.668 250.701 0 137.836-113.095 250.701-250.668 250.701C118.105 506.402 5 393.31 5 255.701Z"
      clipRule="evenodd"
    />
    <path
      fill="#FF4A00"
      d="M374.48 314.879c-9.467 48.288-52.542 86.398-102.244 89.475-51.833 3.551-100.825-23.671-119.522-75.036-5.68-15.15-7.1-31.009-7.1-47.579v-46.868c0-14.439 4.97-19.41 19.407-19.41h81.417v-.71h79.997c12.307 0 24.614.71 36.684.71 7.101 0 13.728 4.971 13.728 12.309.473 30.062 3.55 58.94-2.367 87.109ZM212.356 201.021c17.278.71 22.958-1.42 23.668-12.309.71-19.41.71-38.11 0-57.757 0-7.101-5.68-12.308-12.307-12.308H211.41c-7.811 0-13.018 5.681-13.728 13.729v55.626c.947 7.811 6.154 12.309 14.674 13.019ZM302.53 201.021c16.568 0 22.958-2.84 22.958-13.019v-57.757c0-7.101-5.68-11.598-13.017-12.308h-11.597c-8.757 0-13.728 4.971-13.728 13.729V187.292c.947 8.522 5.917 13.729 15.384 13.729Z"
    />
  </svg>
}
    href="/integrations/integrately"
  >
    Manage contacts and send emails from over a thousand other platforms.
  </Card>

  <Card
    title="Make"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 512 512"
  >
    <g transform="translate(3757 -1767)">
      <circle
        cx="256"
        cy="256"
        r="256"
        fill="#FF4A00"
        data-name="Ellipse 10"
        transform="translate(-3757 1767)"
      />
      <path
        fill="#fff"
        d="M-3431.576 1919.969a7.187 7.187 0 0 0-7.186 7.192v192.191a7.19 7.19 0 0 0 7.186 7.192h52.063a7.187 7.187 0 0 0 7.186-7.192v-192.191a7.183 7.183 0 0 0-7.186-7.192Zm-152.714 2.539-86.296 171.712a7.191 7.191 0 0 0 3.192 9.654l46.514 23.379a7.184 7.184 0 0 0 9.654-3.2l86.3-171.711a7.184 7.184 0 0 0-3.2-9.654l-46.511-23.377a7.2 7.2 0 0 0-9.654 3.2m72.592.614-36.926 187.176a7.189 7.189 0 0 0 5.632 8.442l51.028 10.306a7.2 7.2 0 0 0 8.481-5.665l36.928-187.173a7.19 7.19 0 0 0-5.637-8.442l-51.03-10.306a7.391 7.391 0 0 0-1.427-.144 7.194 7.194 0 0 0-7.048 5.8"
        data-name="Path 141560"
      />
    </g>
  </svg>
}
    href="/integrations/make"
  >
    Manage contacts and send emails from thousands of other platforms.
  </Card>

  <Card
    title="Polar"
    icon={<svg
  viewBox="-0.5 -0.5 16 16"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
  aria-hidden="true"
  height="24"
  width="24"
>
  <path
    d="M7.5 14.337187499999999C3.7239375000000003 14.337187499999999 0.6628125 11.276062499999998 0.6628125 7.5 0.6628125 3.7239375000000003 3.7239375000000003 0.6628125 7.5 0.6628125c3.7760624999999997 0 6.837187500000001 3.061125 6.837187500000001 6.837187500000001 0 3.7760624999999997 -3.061125 6.837187500000001 -6.837187500000001 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M7.5 14.337187499999999c-1.5104375 0 -2.7348749999999997 -3.061125 -2.7348749999999997 -6.837187500000001C4.765125 3.7239375000000003 5.9895625 0.6628125 7.5 0.6628125c1.5103749999999998 0 2.7348749999999997 3.061125 2.7348749999999997 6.837187500000001 0 3.7760624999999997 -1.2245 6.837187500000001 -2.7348749999999997 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M5.4488125 13.653500000000001c-2.051125 -0.6837500000000001 -2.7348749999999997 -3.6845624999999997 -2.7348749999999997 -5.811625 0 -2.1270625 1.025625 -4.7860625 3.418625 -6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M9.551187500000001 1.3464999999999998c2.051125 0.6837500000000001 2.7348749999999997 3.6846250000000005 2.7348749999999997 5.811625 0 2.1270625 -1.025625 4.7860625 -3.418625 6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
</svg>}
    href="/integrations/polar"
  >
    Sync customers to your audience and send automated emails.
  </Card>

  <Card
    title="PostHog"
    icon={
  <div style={{ position: "relative", top: "6px" }}>
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 50 30">
      <path
        fill="#FF4A00"
        d="M10.891 17.206a1 1 0 0 1-1.788 0l-.882-1.763a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763zm0 9.997a1 1 0 0 1-1.788 0L8.22 25.44a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763z"
      />
      <path
        fill="#FF4A00"
        d="M0 23.408c0-.89 1.077-1.337 1.707-.707l4.583 4.583c.63.63.184 1.708-.707 1.708H1a1 1 0 0 1-1-1v-4.584zm0-4.828a1 1 0 0 0 .293.708l9.411 9.41a1 1 0 0 0 .707.294h5.17c.89 0 1.337-1.077.707-1.707l-14.58-14.58C1.077 12.074 0 12.52 0 13.41v5.17zm0-9.997a1 1 0 0 0 .293.707L19.7 28.7a1 1 0 0 0 .707.293h5.17c.89 0 1.337-1.078.707-1.708L1.707 2.707C1.077 2.077 0 2.523 0 3.414v5.17zm9.997 0a1 1 0 0 0 .293.707l17.994 17.995c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707L11.704 2.707c-.63-.63-1.707-.184-1.707.707v5.17zm11.704-5.876c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l7.997 7.998c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707l-7.997-7.997z"
      />
      <path
        fill="#FF4A00"
        d="m42.525 23.53-9.413-9.412c-.63-.63-1.707-.184-1.707.707v13.167a1 1 0 0 0 1 1h14.58a1 1 0 0 0 1-1v-1.2c0-.552-.449-.993-.997-1.064a7.723 7.723 0 0 1-4.463-2.197zm-6.321 2.263a1.6 1.6 0 1 1 0-3.2 1.6 1.6 0 0 1 0 3.2z"
      />
      <path
        fill="#FF4A00"
        d="M0 27.992a1 1 0 0 0 1 1h4.583c.891 0 1.337-1.078.707-1.708l-4.583-4.583c-.63-.63-1.707-.184-1.707.707v4.584zm9.997-16.995-8.29-8.29C1.077 2.077 0 2.523 0 3.414v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998zm-8.29 1.707c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998l-8.29-8.29z"
      />
      <path
        fill="#FF4A00"
        d="M19.994 11.411a1 1 0 0 0-.293-.707l-7.997-7.997c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705V11.41zm-9.997 17.58h5.583c.891 0 1.337-1.077.707-1.707l-6.29-6.29v7.998zm0-17.994v7.583a1 1 0 0 0 .293.708l9.704 9.704v-7.584a1 1 0 0 0-.293-.707l-9.704-9.704z"
      />
    </svg>
  </div>
}
    href="/integrations/posthog"
  >
    Sync users and trigger workflows from PostHog.
  </Card>

  <Card title="RudderStack" icon={<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 55.9 56"><path d="M0 2.7C0 1.2 1.2 0 2.6 0h51.7c.9 0 1.6.7 1.6 1.6v51.7c0 1.5-1.2 2.7-2.7 2.7C23.8 56 0 32.1 0 2.7" fill="#FF4A00" /></svg>} href="/integrations/rudderstack">
    Manage contacts and trigger workflows from hundreds of other platforms.
  </Card>

  <Card title="Salesforce" icon="salesforce" href="/integrations/auto-bcc">
    Sync outgoing mail and contacts to Salesforce.
  </Card>

  <Card
    title="Segment"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85.574 89.423">
    <g fill="#FF4A00" data-name="Layer 1">
      <rect width="52.25" height="9.064" x="32.817" y="28.854" rx="1.685" />
      <rect width="52.25" height="9.064" x=".488" y="51.505" rx="1.685" />
      <path d="M6.761 35.058a1.7 1.7 0 0 0 2.053-1.154A35.66 35.66 0 0 1 50.482 9.903a1.671 1.671 0 0 0 1.958-1.216l1.475-5.504a1.695 1.695 0 0 0-1.282-2.087 44.726 44.726 0 0 0-52.56 30.39 1.677 1.677 0 0 0 1.185 2.097ZM78.813 54.364a1.699 1.699 0 0 0-2.053 1.155A35.659 35.659 0 0 1 35.092 79.52a1.671 1.671 0 0 0-1.958 1.216l-1.474 5.503a1.695 1.695 0 0 0 1.282 2.088 44.726 44.726 0 0 0 52.56-30.39 1.677 1.677 0 0 0-1.185-2.097Z" />
      <circle cx="68.613" cy="13.932" r="4.97" />
      <circle cx="16.959" cy="75.491" r="4.97" />
    </g>
  </svg>
}
    href="/integrations/segment"
  >
    Manage contacts and trigger workflows from thousands of other platforms.
  </Card>

  <Card title="Stripe" icon="stripe-s" href="/integrations/stripe">
    Sync customers to your audience and send automated emails.
  </Card>

  <Card title="Supabase" icon={SupabaseIcon()} href="/integrations/supabase">
    Sync Supabase users to your audience and send auth emails.
  </Card>

  <Card
    title="Webflow"
    icon={
  <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M15.3334 4L11.2916 12H7.49528L9.18675 8.68446H9.11086C7.7154 10.5186 5.63335 11.726 2.66675 12V8.73034C2.66675 8.73034 4.56456 8.61685 5.68023 7.42922H2.66675V4.00006H6.05357V6.8205L6.12959 6.82018L7.51356 4.00006H10.0749V6.80261L10.151 6.80249L11.5869 4H15.3334Z"
      fill="#FF4A00"
    ></path>
  </svg>
}
    href="/integrations/webflow"
  >
    Add a Loops signup form to your Webflow site.
  </Card>

  <Card
    title="Zapier"
    icon={
  <svg
    viewBox="0 0 256 256"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
  >
    <g>
      <path
        d="M128.080089,-0.000183105 C135.311053,0.0131003068 142.422517,0.624138494 149.335663,1.77979593 L149.335663,1.77979593 L149.335663,76.2997796 L202.166953,23.6044907 C208.002065,27.7488446 213.460883,32.3582023 218.507811,37.3926715 C223.557281,42.4271407 228.192318,47.8867213 232.346817,53.7047992 L232.346817,53.7047992 L179.512985,106.400063 L254.227854,106.400063 C255.387249,113.29414 256,120.36111 256,127.587243 L256,127.587243 L256,127.759881 C256,134.986013 255.387249,142.066204 254.227854,148.960282 L254.227854,148.960282 L179.500273,148.960282 L232.346817,201.642324 C228.192318,207.460402 223.557281,212.919983 218.523066,217.954452 L218.523066,217.954452 L218.507811,217.954452 C213.460883,222.988921 208.002065,227.6115 202.182208,231.742607 L202.182208,231.742607 L149.335663,179.04709 L149.335663,253.5672 C142.435229,254.723036 135.323765,255.333244 128.092802,255.348499 L128.092802,255.348499 L127.907197,255.348499 C120.673691,255.333244 113.590195,254.723036 106.677048,253.5672 L106.677048,253.5672 L106.677048,179.04709 L53.8457596,231.742607 C42.1780766,223.466917 31.977435,213.278734 23.6658953,201.642324 L23.6658953,201.642324 L76.4997269,148.960282 L1.78485803,148.960282 C0.612750404,142.052729 0,134.946095 0,127.719963 L0,127.719963 L0,127.349037 C0.0121454869,125.473817 0.134939797,123.182933 0.311311815,120.812834 L0.36577283,120.099764 C0.887996182,113.428547 1.78485803,106.400063 1.78485803,106.400063 L1.78485803,106.400063 L76.4997269,106.400063 L23.6658953,53.7047992 C27.8076812,47.8867213 32.4300059,42.4403618 37.4769335,37.4193681 L37.4769335,37.4193681 L37.5023588,37.3926715 C42.5391163,32.3582023 48.0106469,27.7488446 53.8457596,23.6044907 L53.8457596,23.6044907 L106.677048,76.2997796 L106.677048,1.77979593 C113.590195,0.624138494 120.688946,0.0131003068 127.932622,-0.000183105 L127.932622,-0.000183105 L128.080089,-0.000183105 Z M128.067377,95.7600714 L127.945335,95.7600714 C118.436262,95.7600714 109.32891,97.5001809 100.910584,100.661566 C97.7553011,109.043534 96.0085811,118.129275 95.9958684,127.613685 L95.9958684,127.733184 C96.0085811,137.217594 97.7553011,146.303589 100.923296,154.685303 C109.32891,157.846943 118.436262,159.587052 127.945335,159.587052 L128.067377,159.587052 C137.576449,159.587052 146.683802,157.846943 155.089415,154.685303 C158.257411,146.290368 160.004131,137.217594 160.004131,127.733184 L160.004131,127.613685 C160.004131,118.129275 158.257411,109.043534 155.089415,100.661566 C146.683802,97.5001809 137.576449,95.7600714 128.067377,95.7600714 Z"
        fill="#FF4A00"
        fillRule="nonzero"
      ></path>
    </g>
  </svg>
}
    href="/integrations/zapier"
  >
    Manage contacts and send emails from thousands of other platforms.
  </Card>
</CardGroup>

## Send email

<CardGroup>
  <Card
    title="Auth0"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 256 285"
  >
    <path
      fill="#FF4A00"
      d="M220.412 0h-92.415l28.562 89.006h92.416l-74.77 53.077 28.57 89.511c48.128-35.06 63.854-88.12 46.208-142.588L220.413 0ZM7.018 89.006h92.416L127.997 0H35.589L7.019 89.006c-17.655 54.468-1.92 107.529 46.207 142.588l28.563-89.51-74.77-53.078Zm46.208 142.588 74.77 52.97 74.77-52.97-74.77-53.847-74.77 53.847Z"
    />
  </svg>
}
    href="/integrations/auth0"
  >
    Send Auth0 authentication emails with Loops.
  </Card>

  <Card title="Auth.js" icon="shield" href="/integrations/authjs">
    Send Auth.js magic link emails with Loops.
  </Card>

  <Card
    title="Bubble"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17">
    <g fill="none" fillRule="nonzero" transform="translate(.5 .111)">
      <path
        fill="#FF4A00"
        d="M9.361 4.277c-1.474 0-2.928.633-4.037 1.877V.016H3.128v10.336a6.077 6.077 0 0 0 12.151 0c0-3.354-2.563-6.075-5.918-6.075Zm-.157 9.81a3.735 3.735 0 1 1 0-7.47 3.735 3.735 0 0 1 0 7.47Z"
      />
      <circle cx="1.53" cy="14.925" r="1.503" fill="#FF4A00" />
    </g>
  </svg>
}
    href="/integrations/bubble"
  >
    Manage contacts and send emails from your Bubble app.
  </Card>

  <Card
    title="Clerk"
    icon={
  <svg viewBox="0 0 18 18" fill="none" aria-hidden="true">
    <ellipse
      cx="8.99999"
      cy="9"
      rx="2.81249"
      ry="2.8125"
      fill="#FF4A00"
    ></ellipse>
    <path
      fill="#FF4A00"
      d="M14.0674 15.6591C14.3067 15.8984 14.2827 16.2945 14.0015 16.4829C12.571 17.4411 10.8504 17.9999 8.9993 17.9999C7.14818 17.9999 5.42758 17.4411 3.99704 16.4829C3.71589 16.2945 3.69186 15.8984 3.93115 15.6591L5.98648 13.6037C6.17225 13.418 6.46042 13.3886 6.69424 13.5084C7.3856 13.8626 8.16911 14.0624 8.9993 14.0624C9.82948 14.0624 10.613 13.8626 11.3044 13.5084C11.5382 13.3886 11.8263 13.418 12.0121 13.6037L14.0674 15.6591Z"
    ></path>
    <path
      fill="#FF4A00"
      d="M14.0022 1.51706C14.2834 1.70539 14.3074 2.10155 14.0681 2.34084L12.0128 4.39619C11.827 4.58195 11.5388 4.61129 11.305 4.49151C10.6136 4.13733 9.83014 3.9375 8.99996 3.9375C6.20403 3.9375 3.93748 6.20406 3.93748 9C3.93748 9.83018 4.13731 10.6137 4.49149 11.3051C4.61127 11.5389 4.58193 11.827 4.39617 12.0128L2.34083 14.0682C2.10154 14.3074 1.70538 14.2834 1.51705 14.0023C0.558857 12.5717 0 10.8511 0 9C0 4.02944 4.02942 0 8.99996 0C10.8511 0 12.5717 0.55886 14.0022 1.51706Z"
      fillOpacity="0.5"
    ></path>
  </svg>
}
    href="/integrations/clerk"
  >
    Sync users to your audience and send automated emails.
  </Card>

  <Card
    title="Integrately"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 512 512">
    <path
      fill="#FF4A00"
      fillRule="evenodd"
      d="M256.379 20.676c-129.891 0-235.703 105.401-235.703 235.025 0 128.924 106.059 235.024 235.703 235.024 128.924 0 234.992-105.854 234.992-235.024 0-128.939-106.073-235.025-234.992-235.025ZM5 255.701C5 117.371 117.878 5 256.379 5c137.578 0 250.668 113.106 250.668 250.701 0 137.836-113.095 250.701-250.668 250.701C118.105 506.402 5 393.31 5 255.701Z"
      clipRule="evenodd"
    />
    <path
      fill="#FF4A00"
      d="M374.48 314.879c-9.467 48.288-52.542 86.398-102.244 89.475-51.833 3.551-100.825-23.671-119.522-75.036-5.68-15.15-7.1-31.009-7.1-47.579v-46.868c0-14.439 4.97-19.41 19.407-19.41h81.417v-.71h79.997c12.307 0 24.614.71 36.684.71 7.101 0 13.728 4.971 13.728 12.309.473 30.062 3.55 58.94-2.367 87.109ZM212.356 201.021c17.278.71 22.958-1.42 23.668-12.309.71-19.41.71-38.11 0-57.757 0-7.101-5.68-12.308-12.307-12.308H211.41c-7.811 0-13.018 5.681-13.728 13.729v55.626c.947 7.811 6.154 12.309 14.674 13.019ZM302.53 201.021c16.568 0 22.958-2.84 22.958-13.019v-57.757c0-7.101-5.68-11.598-13.017-12.308h-11.597c-8.757 0-13.728 4.971-13.728 13.729V187.292c.947 8.522 5.917 13.729 15.384 13.729Z"
    />
  </svg>
}
    href="/integrations/integrately"
  >
    Manage contacts and send emails from over a thousand other platforms.
  </Card>

  <Card
    title="Make"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 512 512"
  >
    <g transform="translate(3757 -1767)">
      <circle
        cx="256"
        cy="256"
        r="256"
        fill="#FF4A00"
        data-name="Ellipse 10"
        transform="translate(-3757 1767)"
      />
      <path
        fill="#fff"
        d="M-3431.576 1919.969a7.187 7.187 0 0 0-7.186 7.192v192.191a7.19 7.19 0 0 0 7.186 7.192h52.063a7.187 7.187 0 0 0 7.186-7.192v-192.191a7.183 7.183 0 0 0-7.186-7.192Zm-152.714 2.539-86.296 171.712a7.191 7.191 0 0 0 3.192 9.654l46.514 23.379a7.184 7.184 0 0 0 9.654-3.2l86.3-171.711a7.184 7.184 0 0 0-3.2-9.654l-46.511-23.377a7.2 7.2 0 0 0-9.654 3.2m72.592.614-36.926 187.176a7.189 7.189 0 0 0 5.632 8.442l51.028 10.306a7.2 7.2 0 0 0 8.481-5.665l36.928-187.173a7.19 7.19 0 0 0-5.637-8.442l-51.03-10.306a7.391 7.391 0 0 0-1.427-.144 7.194 7.194 0 0 0-7.048 5.8"
        data-name="Path 141560"
      />
    </g>
  </svg>
}
    href="/integrations/make"
  >
    Manage contacts and send emails from thousands of other platforms.
  </Card>

  <Card
    title="Polar"
    icon={<svg
  viewBox="-0.5 -0.5 16 16"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
  aria-hidden="true"
  height="24"
  width="24"
>
  <path
    d="M7.5 14.337187499999999C3.7239375000000003 14.337187499999999 0.6628125 11.276062499999998 0.6628125 7.5 0.6628125 3.7239375000000003 3.7239375000000003 0.6628125 7.5 0.6628125c3.7760624999999997 0 6.837187500000001 3.061125 6.837187500000001 6.837187500000001 0 3.7760624999999997 -3.061125 6.837187500000001 -6.837187500000001 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M7.5 14.337187499999999c-1.5104375 0 -2.7348749999999997 -3.061125 -2.7348749999999997 -6.837187500000001C4.765125 3.7239375000000003 5.9895625 0.6628125 7.5 0.6628125c1.5103749999999998 0 2.7348749999999997 3.061125 2.7348749999999997 6.837187500000001 0 3.7760624999999997 -1.2245 6.837187500000001 -2.7348749999999997 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M5.4488125 13.653500000000001c-2.051125 -0.6837500000000001 -2.7348749999999997 -3.6845624999999997 -2.7348749999999997 -5.811625 0 -2.1270625 1.025625 -4.7860625 3.418625 -6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M9.551187500000001 1.3464999999999998c2.051125 0.6837500000000001 2.7348749999999997 3.6846250000000005 2.7348749999999997 5.811625 0 2.1270625 -1.025625 4.7860625 -3.418625 6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
</svg>}
    href="/integrations/polar"
  >
    Sync customers to your audience and send automated emails.
  </Card>

  <Card
    title="PostHog"
    icon={
  <div style={{ position: "relative", top: "6px" }}>
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 50 30">
      <path
        fill="#FF4A00"
        d="M10.891 17.206a1 1 0 0 1-1.788 0l-.882-1.763a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763zm0 9.997a1 1 0 0 1-1.788 0L8.22 25.44a1 1 0 0 1 0-.894l.882-1.763a1 1 0 0 1 1.788 0l.882 1.763a1 1 0 0 1 0 .894l-.882 1.763z"
      />
      <path
        fill="#FF4A00"
        d="M0 23.408c0-.89 1.077-1.337 1.707-.707l4.583 4.583c.63.63.184 1.708-.707 1.708H1a1 1 0 0 1-1-1v-4.584zm0-4.828a1 1 0 0 0 .293.708l9.411 9.41a1 1 0 0 0 .707.294h5.17c.89 0 1.337-1.077.707-1.707l-14.58-14.58C1.077 12.074 0 12.52 0 13.41v5.17zm0-9.997a1 1 0 0 0 .293.707L19.7 28.7a1 1 0 0 0 .707.293h5.17c.89 0 1.337-1.078.707-1.708L1.707 2.707C1.077 2.077 0 2.523 0 3.414v5.17zm9.997 0a1 1 0 0 0 .293.707l17.994 17.995c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707L11.704 2.707c-.63-.63-1.707-.184-1.707.707v5.17zm11.704-5.876c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l7.997 7.998c.63.63 1.707.183 1.707-.708v-5.169a1 1 0 0 0-.293-.707l-7.997-7.997z"
      />
      <path
        fill="#FF4A00"
        d="m42.525 23.53-9.413-9.412c-.63-.63-1.707-.184-1.707.707v13.167a1 1 0 0 0 1 1h14.58a1 1 0 0 0 1-1v-1.2c0-.552-.449-.993-.997-1.064a7.723 7.723 0 0 1-4.463-2.197zm-6.321 2.263a1.6 1.6 0 1 1 0-3.2 1.6 1.6 0 0 1 0 3.2z"
      />
      <path
        fill="#FF4A00"
        d="M0 27.992a1 1 0 0 0 1 1h4.583c.891 0 1.337-1.078.707-1.708l-4.583-4.583c-.63-.63-1.707-.184-1.707.707v4.584zm9.997-16.995-8.29-8.29C1.077 2.077 0 2.523 0 3.414v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998zm-8.29 1.707c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705v-7.998l-8.29-8.29z"
      />
      <path
        fill="#FF4A00"
        d="M19.994 11.411a1 1 0 0 0-.293-.707l-7.997-7.997c-.63-.63-1.707-.184-1.707.707v5.17a1 1 0 0 0 .293.706l9.704 9.705V11.41zm-9.997 17.58h5.583c.891 0 1.337-1.077.707-1.707l-6.29-6.29v7.998zm0-17.994v7.583a1 1 0 0 0 .293.708l9.704 9.704v-7.584a1 1 0 0 0-.293-.707l-9.704-9.704z"
      />
    </svg>
  </div>
}
    href="/integrations/posthog"
  >
    Sync users and trigger workflows from PostHog.
  </Card>

  <Card
    title="RudderStack"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 55.9 56"><path d="M0 2.7C0 1.2 1.2 0 2.6 0h51.7c.9 0 1.6.7 1.6 1.6v51.7c0 1.5-1.2 2.7-2.7 2.7C23.8 56 0 32.1 0 2.7" fill="#FF4A00" /></svg>
}
    href="/integrations/rudderstack"
  >
    Manage contacts and trigger workflows from hundreds of other platforms.
  </Card>

  <Card
    title="Segment"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85.574 89.423">
    <g fill="#FF4A00" data-name="Layer 1">
      <rect width="52.25" height="9.064" x="32.817" y="28.854" rx="1.685" />
      <rect width="52.25" height="9.064" x=".488" y="51.505" rx="1.685" />
      <path d="M6.761 35.058a1.7 1.7 0 0 0 2.053-1.154A35.66 35.66 0 0 1 50.482 9.903a1.671 1.671 0 0 0 1.958-1.216l1.475-5.504a1.695 1.695 0 0 0-1.282-2.087 44.726 44.726 0 0 0-52.56 30.39 1.677 1.677 0 0 0 1.185 2.097ZM78.813 54.364a1.699 1.699 0 0 0-2.053 1.155A35.659 35.659 0 0 1 35.092 79.52a1.671 1.671 0 0 0-1.958 1.216l-1.474 5.503a1.695 1.695 0 0 0 1.282 2.088 44.726 44.726 0 0 0 52.56-30.39 1.677 1.677 0 0 0-1.185-2.097Z" />
      <circle cx="68.613" cy="13.932" r="4.97" />
      <circle cx="16.959" cy="75.491" r="4.97" />
    </g>
  </svg>
}
    href="/integrations/segment"
  >
    Manage contacts and trigger workflows from thousands of other platforms.
  </Card>

  <Card title="Stripe" icon="stripe-s" href="/integrations/stripe">
    Sync customers to your audience and send automated emails.
  </Card>

  <Card title="Supabase" icon={SupabaseIcon()} href="/smtp/supabase">
    Send Supabase authentication emails with Loops.
  </Card>

  <Card
    title="Zapier"
    icon={
  <svg
    viewBox="0 0 256 256"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
  >
    <g>
      <path
        d="M128.080089,-0.000183105 C135.311053,0.0131003068 142.422517,0.624138494 149.335663,1.77979593 L149.335663,1.77979593 L149.335663,76.2997796 L202.166953,23.6044907 C208.002065,27.7488446 213.460883,32.3582023 218.507811,37.3926715 C223.557281,42.4271407 228.192318,47.8867213 232.346817,53.7047992 L232.346817,53.7047992 L179.512985,106.400063 L254.227854,106.400063 C255.387249,113.29414 256,120.36111 256,127.587243 L256,127.587243 L256,127.759881 C256,134.986013 255.387249,142.066204 254.227854,148.960282 L254.227854,148.960282 L179.500273,148.960282 L232.346817,201.642324 C228.192318,207.460402 223.557281,212.919983 218.523066,217.954452 L218.523066,217.954452 L218.507811,217.954452 C213.460883,222.988921 208.002065,227.6115 202.182208,231.742607 L202.182208,231.742607 L149.335663,179.04709 L149.335663,253.5672 C142.435229,254.723036 135.323765,255.333244 128.092802,255.348499 L128.092802,255.348499 L127.907197,255.348499 C120.673691,255.333244 113.590195,254.723036 106.677048,253.5672 L106.677048,253.5672 L106.677048,179.04709 L53.8457596,231.742607 C42.1780766,223.466917 31.977435,213.278734 23.6658953,201.642324 L23.6658953,201.642324 L76.4997269,148.960282 L1.78485803,148.960282 C0.612750404,142.052729 0,134.946095 0,127.719963 L0,127.719963 L0,127.349037 C0.0121454869,125.473817 0.134939797,123.182933 0.311311815,120.812834 L0.36577283,120.099764 C0.887996182,113.428547 1.78485803,106.400063 1.78485803,106.400063 L1.78485803,106.400063 L76.4997269,106.400063 L23.6658953,53.7047992 C27.8076812,47.8867213 32.4300059,42.4403618 37.4769335,37.4193681 L37.4769335,37.4193681 L37.5023588,37.3926715 C42.5391163,32.3582023 48.0106469,27.7488446 53.8457596,23.6044907 L53.8457596,23.6044907 L106.677048,76.2997796 L106.677048,1.77979593 C113.590195,0.624138494 120.688946,0.0131003068 127.932622,-0.000183105 L127.932622,-0.000183105 L128.080089,-0.000183105 Z M128.067377,95.7600714 L127.945335,95.7600714 C118.436262,95.7600714 109.32891,97.5001809 100.910584,100.661566 C97.7553011,109.043534 96.0085811,118.129275 95.9958684,127.613685 L95.9958684,127.733184 C96.0085811,137.217594 97.7553011,146.303589 100.923296,154.685303 C109.32891,157.846943 118.436262,159.587052 127.945335,159.587052 L128.067377,159.587052 C137.576449,159.587052 146.683802,157.846943 155.089415,154.685303 C158.257411,146.290368 160.004131,137.217594 160.004131,127.733184 L160.004131,127.613685 C160.004131,118.129275 158.257411,109.043534 155.089415,100.661566 C146.683802,97.5001809 137.576449,95.7600714 128.067377,95.7600714 Z"
        fill="#FF4A00"
        fillRule="nonzero"
      ></path>
    </g>
  </svg>
}
    href="/integrations/zapier"
  >
    Manage contacts and send emails from thousands of other platforms.
  </Card>
</CardGroup>

## Create templates

<CardGroup>
  <Card title="Emailify" icon="upload" href="/creating-emails/uploading-custom-email#emailify">
    Import custom MJML email templates from Emailify.
  </Card>

  <Card title="Email Love" icon="upload" href="/creating-emails/uploading-custom-email#email-love">
    Import custom MJML email templates from Email Love.
  </Card>
</CardGroup>


# Auth0 integration
Source: https://loops.so/docs/integrations/auth0

Send Auth0 authentication emails with Loops.

Set up an SMTP connection to send all of your Auth0 emails with Loops.

There are two big benefits to using Loops for sending your Auth0 emails:

<Steps>
  <Step title="More control over design">
    You can use [Loops' design editor](/docs/creating-emails/editor) to create (and
    then easily edit) beautiful transactional emails instead of having to code
    them with HTML.
  </Step>

  <Step title="Better visibility of sent emails">
    You get full visibility on which emails are being sent, when, and to whom in
    your Loops account. Auth0 doesn't offer this view.
  </Step>
</Steps>

## Set up Loops SMTP in Auth0

Go to **Branding -> Email Provider** in your Auth0 dashboard.

Scroll down and click on **SMTP Provider**.

In the SMTP Provider Settings section below, enter a value into the "From" field. This value will *always be overwritten by the values set in your Loops templates* from the next step, so it can be anything.

In the **SMTP Provider Settings** section enter the following data:

| Field       | Value                                                                                       |
| ----------- | ------------------------------------------------------------------------------------------- |
| Host        | `smtp.loops.so`                                                                             |
| Port number | `587`                                                                                       |
| Username    | `loops`                                                                                     |
| Password    | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops |

<img alt="Auth0 SMTP provider settings configured for Loops" />

<Info>
  The **Send Test Email** button here will not work due to how the Loops SMTP
  system works. You can test your connection in a later step.
</Info>

## Create Transactional emails in Loops

Next, create new transactional emails for the emails you are sending from Auth0. Go to **Branding -> Email Templates** to view the full list.

In Loops, go to the [Transactional page](https://app.loops.so/transactional) and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates).

<img alt="Creating a transactional email template in Loops" />

You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like.

You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails.

For each Loops template you create, you need to [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Auth0 to be inserted into each email.

You can check the list of [Common variables](https://auth0.com/docs/customize/email/email-templates#common-variables) supported in each email from the Auth0 documentation.

Once you're done creating the email and adding the data variables, click **Next**. On the next page, click the **Show payload** button to view the API payload for your template. You will need this for the next step.

<img alt="Viewing the transactional email payload in Loops" />

Make sure to also publish your email! It won't send unless it's published.

<CardGroup>
  <Card title="Transactional email guide" icon="code" href="/transactional">
    Read our detailed guide for sending transactional emails.
  </Card>
</CardGroup>

## Configure email templates in Auth0

The final step is to make sure your emails in Auth0 are configured to send the correct data to Loops.

<Warning>
  Make sure you set up at least the **Verification Email (using Link)** or
  **Verification Email (using code)** templates in Auth0. Enable other emails
  based on your user flows.
</Warning>

Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data.

In Auth0, go to **Branding -> Email Templates**, then edit each template to contain the payload as shown in the previous step (you can click the clipboard icon in Loops to copy the full payload).

<Tip>
  Make sure that each template you want to use in Auth0 has the **Status** field
  enabled.
</Tip>

If you are using Passwordless authentication, add your Loops payload into the **Body** field at **Authentication -> Passwordless -> Email**.

Once pasted into the **Message** body, you need to add the Auth0 message variables into the payload. You can do this using double curly brackets like `{{ url }}`.

Here is an example "Verification Email (using Link)" email template. This payload was copied from the template's Publish page in Loops, then the `{{ user.email }}` and `{{ url }}` Auth0 variables were added.

```json theme={"dark"}
{
  "transactionalId": "clvmzp39u035tl50pw7wrl0ri",
  "email": "{{ user.email }}",
  "dataVariables": {
    "productName": "{{ application.name }}",
    "url": "{{ url }}"
  }
}
```

If you want to add each Auth0 user to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ user.email }}` value.

```json {4} theme={"dark"}
{
  "transactionalId": "clvmzp39u035tl50pw7wrl0ri",
  "email": "{{ user.email }}",
  "addToAudience": true,
  "dataVariables": {
    "productName": "{{ application.name }}",
    "url": "{{ url }}"
  }
}
```

Here's how the template looks in the Auth0 editor:

<img alt="Auth0 email template editor containing the Loops payload JSON" />

To test that everything works, click the **Try** button beneath the editor. Insert your email address in the modal that appears, then click **Try** to send the email.

You will also be able to see activity for your email sends in **Monitoring -> Logs**.

The best way to view your Auth0 email history is in Loops. Go to your [Transactional](https://app.loops.so/transactional) page then click on one of your emails. Click on **Metrics** in the left menu to view a page containing a table showing all sends and some statistics.

## Important notes

* The subject in Auth0 templates is always overwritten by the subject added to the corresponding template in Loops.
* The sender email configured in your Auth0 SMTP settings is always overwritten by the "From" address added to your templates in Loops.
* Any enabled Auth0 template not set up with the correct API-like payload will fail to send.


# Auth.js
Source: https://loops.so/docs/integrations/authjs

Send Auth.js magic link emails with Loops.

Use Loops to send your [Auth.js Magic Link](https://authjs.dev/getting-started/authentication/email) emails.

<Info>
  You must configure a [database
  adaptor](https://authjs.dev/getting-started/database) to use Magic Link
  authentication in Auth.js.
</Info>

There are two big benefits to using Loops for sending your Auth.js emails:

<Steps>
  <Step title="More control over design">
    You can use [Loops' design editor](/docs/creating-emails/editor) to create (and
    then easily edit) beautiful transactional emails instead of having to code
    them with HTML.
  </Step>

  <Step title="Better visibility of sent emails">
    You get full visibility on which emails are being sent, when, and to whom in
    your Loops account. Auth.js doesn't offer this out of the box.
  </Step>
</Steps>

## Create a transactional email

In Loops, create a new transactional email, which will be sent to your users when they attempt to log in.

Here is where you define your email's subject and sending details like From address and Reply to address. (Click the `>` button to reveal all the sending options.)

Then you can create your email in the editor. We recommend creating a stylish but basic email for magic links, to help with deliverability. Use the [style panel](/docs/creating-emails/styles#style-panel) to customize the design.

To add the Auth.js magic link in your email, add a [data variable](/docs/transactional#add-data-variables) named `url`. When the email is sent, each user's magic link will be inserted wherever you add the variable.

You can add data variables as URLs to text, buttons and images. In this example, we're adding the `url` data variable into the button's link field.

<Warning>
  Make sure that your variable is named `url`, which is the variable name
  Auth.js uses when sending authentication emails.
</Warning>

<img alt="Magic link button" />

When you're done make sure to Save and Publish your email. Unpublished emails will not be sent.

## Set up Loops in your Auth.js Project

<Steps>
  <Step title="Environment variables">
    To use the Loops provider in Auth.js, you'll need to add two environment variables to your project.

    The first is an API key. You can generate or copy one from [Settings -> API](https://app.loops.so/settings?page=api).

    The second is the ID of your transactional email. Go to the email in Loops and click on to the **Publish** page. Here you will find the **Transactional ID**.

    <img alt="Publish page with details" />

    Add both values to your environment, for example in an `.env` file:

    ```
    AUTH_LOOPS_API_KEY=<API_KEY>
    AUTH_LOOPS_TRANSACTIONAL_ID=<TRANSACTIONAL_ID>
    ```
  </Step>

  <Step title="Use the Loops provider">
    The final step is to configure your project to send emails with the built-in Loops provider.

    ```javascript auth.js theme={"dark"}
    import NextAuth from "next-auth"
    import Loops from "next-auth/providers/loops"

    export const { handlers, auth, signIn, signOut } = NextAuth({
      adapter: ..., // database adapter of your choosing
      providers: [
        Loops({
          apiKey: process.env.AUTH_LOOPS_API_KEY,
          transactionalId: process.env.AUTH_LOOPS_TRANSACTIONAL_ID,
        }),
      ],
    })
    ```
  </Step>
</Steps>

That's it!

Don't forget, your email details (subject, from address etc) and content can all be managed from Loops.

You also get access to [transactional email logs](/docs/transactional#metrics) giving you an eye on your sending history.


# Auto BCC
Source: https://loops.so/docs/integrations/auto-bcc

Import emails and contacts to your CRM by adding a BCC address to your marketing emails.

<Tip>
  Our Auto BCC feature works with campaigns and workflows, but is not offered on
  transactional emails.
</Tip>

<Warning>
  By adding a BCC email address in your settings, your emails will be BCCd to
  this address. Do not use a personal email address, only one generated by your
  CRM.
</Warning>

## Overview

Auto BCC is a feature that imports emails and contacts into your CRM platform. This works by adding a BCC email address to all outgoing campaign and workflow emails sent from Loops.

You can add a BCC email address in your [Sending settings page](https://app.loops.so/settings?page=sending).

Be aware that BCCing might not be enough by itself. Some platforms may require you to add the sender email address to an allowlist or for the sender to be a user on your account.

<img alt="Auto BCC setting in Loops sending settings" />

## Syncing with Salesforce

You can record all outgoing email and add contacts into Salesforce using a pre-generated Email to Salesforce address.

[Find your BCC email address](https://support.cloudhq.net/where-do-i-find-my-bcc-address-in-salesforce/) and paste it into the **BCC email address** field in Loops.

[Read how Email to Salesforce works](https://help.salesforce.com/s/articleView?id=sf.email_my_email_2_sfdc.htm\&type=5)

## Syncing with HubSpot

You can log all outgoing emails and add new contacts to HubSpot using your pre-generated BCC address.

[Find your BCC email address](https://knowledge.hubspot.com/connected-email/log-email-in-your-crm-with-the-bcc-or-forwarding-address#use-the-bcc-address) and paste it into the **BCC email address** field in Loops.

<Tip>
  Make sure to read the "Requirements to log emails to the CRM using the BCC
  address" section on the page above to make sure the sync works correctly.
</Tip>

<Note>
  Auto BCC only syncs contacts and outgoing mail **from Loops to HubSpot**. To
  bring existing HubSpot contacts **into Loops**, export them from HubSpot as a
  CSV and [import the CSV](/docs/add-users/csv-upload), or sync them with
  [Zapier](/docs/integrations/zapier), [Make](/docs/integrations/make), or the
  [Loops API](/docs/add-users/loops-api).
</Note>

## Syncing with Attio

Using Attio's [Email forwarding](https://attio.com/help/reference/email-calendar/email-forwarding) feature, you can sync outgoing emails to your Attio account.

[Find your forwarding email address](https://attio.com/help/reference/email-calendar/email-forwarding#finding-your-inbound-email-address) and paste it into the **BCC email address** field in Loops.

<Tip>
  Your Attio log in email has to match the sending email in Loops. For example,
  if your sending domain is `mail.mydomain.com` and the ["From"
  address](/docs/sending-first-email#sending-settings) for your email is
  `test@example.com`, your Attio account email has to be
  `test@example.com` for this integration to work.
</Tip>

## Syncing with other CRMs

Most CRMs have a feature that lets you import email and contacts. Look for a BCC email address and add it to your Loops settings.


# Better Auth
Source: https://loops.so/docs/integrations/better-auth

Use Loops as the email sender for Better Auth authentication emails.

This guide shows how to use Loops to send [Better Auth](https://better-auth.com) authentication emails.

For this guide, you will need a working app with Better Auth installed and a [Loops](https://app.loops.so) account with a verified sending domain set up.

## Create transactional emails in Loops

The first step is to create transactional templates in Loops for each auth message.

In this guide we have three example emails for the [Email & Password](https://better-auth.com/docs/authentication/email-password), [Magic Link](https://better-auth.com/docs/plugins/magic-link) and [Email OTP](https://better-auth.com/docs/plugins/email-otp) authentication methods.

For the authentication method you are using, you should [create transactional emails](/docs/transactional) for these emails in Loops.

Include [data variables](/docs/transactional#add-data-variables) for the data that will be passed from Better Auth. You can add data variables by typing "\{", via the `{}` button above the editor, or by using the `/` slash menu.

<img alt="Adding data variables to a transactional email" />

After publishing each transactional email, note the **Transactional ID** from the **Review** page. You will need this for the next step.

<img alt="Copying the transactional ID" />

<Tip>
  The following variable names are suggestions and can be changed to whatever you want. Just make sure to use the same names in your Better Auth config code and in your Loops emails.
</Tip>

### Email & Password data variables

Depending on the authentication method you are using, you will need to add different data variables to your emails.

#### Verification email

This email is sent when a user is requested to verify their email address.

* `verificationUrl`
* `token`

#### Password reset email

This email is sent when a user requests to reset their password.

* `resetPasswordUrl`
* `token`

### Magic link data variables

This email is sent when a user signs in with a magic link.

* `magicLinkUrl`
* `token`

### Email OTP data variables

#### Sign in email

This email is sent when a user signs in using an email OTP.

* `otpCode`

#### Email verification email

This email is sent when a user needs to verify their email using an email OTP.

* `otpCode`

#### Password reset email

This email is sent when a user resets their password using an email OTP.

* `otpCode`

## Add the Loops SDK

Install the [Loops SDK](/docs/sdks/javascript) in your project. This will allow you to send transactional emails via Loops.

```bash theme={"dark"}
npm i loops
```

Next you need to add some environment variables to your project, for example in an `.env` file.

You can create an API key from your [API Settings](https://app.loops.so/settings?page=api) page.

The transactional IDs are the IDs of the transactional emails you created in the previous step. (These are not all required, you will only need the ones for the authentication method you are using.)

```bash theme={"dark"}
LOOPS_API_KEY=replace-with-your-loops-api-key

# Email & Password
LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID=replace-with-verify-template-id
LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID=replace-with-reset-template-id

# Magic Link
LOOPS_MAGIC_LINK_TRANSACTIONAL_ID=replace-with-magic-link-template-id

# Email OTP
LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID=replace-with-email-otp-signin-template-id
LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID=replace-with-email-otp-verification-template-id
LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID=replace-with-email-otp-password-reset-template-id
```

## Create a Loops server helper

Next, create a helper function to keep email sending logic clean and reusable. This helper will be used in the next step to configure Better Auth to send emails through Loops.

<Warning>
  It is important that this helper is used server-side only. Never expose your Loops API key in client-side code.
</Warning>

```typescript lib/loops.ts theme={"dark"}
import { LoopsClient } from "loops";

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

type DataVariables = Record<string, string | number | boolean | null>;

export async function sendLoopsTransactionalEmail(input: {
  email: string;
  dataVariables?: DataVariables;
}) {
  const { transactionalId, email, dataVariables } = input;
  transactionalId: string;

  const response = await loops.sendTransactionalEmail({
    transactionalId,
    email,
    dataVariables,
  });

  if (!response.success) {
    throw new Error(response.message || "Failed to send Loops transactional email");
  }
}
```

## Configure Better Auth to send through Loops

In your Better Auth config, enable the authentication method you want to use and add callbacks to send emails.

Inside the `sendLoopsTransactionalEmail()` calls, we populate the content for the data variables you added in your Loops emails.

<Warning>
  Make sure that the data variable names inside `dataVariables` match the names of the data variables you added in your Loops emails.
</Warning>

<Note>
  Better Auth recommends not awaiting email sends in these callbacks to reduce timing-attack risk and keep auth responses fast, so we use `void`. If your platform supports background work (for example `waitUntil`), you can use that instead.
</Note>

### Password reset

For this email, make sure that `emailAndPassword.enabled` is set to `true`, then add a `sendResetPassword` callback containing our `sendLoopsTransactionalEmail()` helper function.

[Better Auth docs](https://better-auth.com/docs/authentication/email-password#request-password-reset)

```ts auth.ts {8-15} theme={"dark"}
import { betterAuth } from "better-auth";
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({ user, url, token }) => {
      void sendLoopsTransactionalEmail({
        transactionalId: process.env.LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID as string,
        email: user.email,
        dataVariables: {
          resetPasswordUrl: url,
          token,
        },
      });
    },
  },
});
```

### Email verification

For this email, add a `sendVerificationEmail` callback containing our `sendLoopsTransactionalEmail()` helper function.

[Better Auth docs](https://better-auth.com/docs/authentication/email-password#email-verification)

```ts auth.ts {7-14} theme={"dark"}
import { betterAuth } from "better-auth";
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  emailVerification: {
    sendVerificationEmail: async ({ user, url, token }) => {
      void sendLoopsTransactionalEmail({
        transactionalId: process.env.LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID as string,
        email: user.email,
        dataVariables: {
          verificationUrl: url,
          token,
        },
      });
    },
  },
});
```

### Magic link plugin

Add the `magicLink` plugin to your Better Auth config and add a `sendMagicLink` callback containing our `sendLoopsTransactionalEmail()` helper function.

[Better Auth docs](https://better-auth.com/docs/plugins/magic-link)

```ts auth.ts {2,9-16} theme={"dark"}
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins"; 
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  plugins: [
    magicLink: {
      sendMagicLink: async ({ email, token, url }) => {
        void sendLoopsTransactionalEmail({
          transactionalId: process.env.LOOPS_MAGIC_LINK_TRANSACTIONAL_ID as string,
          email,
          dataVariables: {
            magicLinkUrl: url,
            token,
          },
        });
      },
    },
  ],
});
```

### Email OTP plugin

Add the `emailOTP` plugin to your Better Auth config and add a `sendVerificationOTP` callback containing our `sendLoopsTransactionalEmail()` helper function.

There are three types of OTP emails: sign in, email verification and password reset, so you will need to create an email template for each type in Loops.

[Better Auth docs](https://better-auth.com/docs/plugins/email-otp)

```ts auth.ts {2,10-16,18-24,26-32} theme={"dark"}
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins"; 
import { sendLoopsTransactionalEmail } from "./lib/loops";

export const auth = betterAuth({
  plugins: [
    emailOTP: {
      async sendVerificationOTP({ email, otp, type }) { 
        if (type === "sign-in") { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } else if (type === "email-verification") { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } else { 
          void sendLoopsTransactionalEmail({
            transactionalId: process.env.LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID as string,
            email,
            dataVariables: {
              otpCode: otp,
            },
          });
        } 
      }, 
    },
  ],
});
```

## Call Better Auth from your app

Now your app is set up to send Better Auth emails through Loops.

For example, Loops will send a transactional when you [sign in a user with an email OTP](https://better-auth.com/docs/plugins/email-otp#sign-in-with-otp):

```ts theme={"dark"}
const data = await auth.api.sendVerificationOTP({
  body: {
    email: "test@example.com", // required
    type: "sign-in", // required
  },
});
```

Or when you [reset a user's password](https://better-auth.com/docs/authentication/email-password#api-method-request-password-reset):

```ts theme={"dark"}
const data = await auth.api.requestPasswordReset({
  body: {
    email: "test@example.com", // required
    redirectTo: "https://example.com/reset-password",
  },
});
```

## Troubleshooting

If your emails are not sending:

* Confirm each transactional email in Loops is **Published**
* Confirm each data variable name in your Better Auth code matches the name of the data variable in your Loops email
* Check that you're using a valid Loops API, from the correct account
* Check that your sending domain is verified in Loops

## Read more

<CardGroup>
  <Card title="Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
    Loops API endpoint used to send transactional emails.
  </Card>

  <Card title="Send emails from Bolt.new" href="/guides/bolt-emails" icon="bolt">
    Read our guide for sending emails from Bolt.new.
  </Card>

  <Card title="Integrations" href="/integrations" icon="arrows-turn-right">
    Integrate Loops into lots more platforms.
  </Card>
</CardGroup>


# Bubble integration
Source: https://loops.so/docs/integrations/bubble

Connect Bubble to Loops with our plugin to add contacts, trigger workflows, and send transactional email.

The Loops Bubble integration connects your Bubble app to Loops so you can sync contacts, trigger workflows, and send transactional email. For other Loops integrations, see the [integrations overview](/docs/integrations).

<Info>
  Our Bubble plugin lets you:

  * Add, find, update and delete contacts
  * Send events to trigger workflows
  * Send transactional email
</Info>

<Tip>
  Our Bubble plugin is unfortunately limited to what it can do because of how
  Bubble works. If you want more flexibility, such as syncing more contact
  data to Loops, check out our [tutorial for using Bubble's API
  Connector](/docs/guides/bubble-api-connector).
</Tip>

## Install the plugin

Go to the [Loops plugin](https://bubble.io/plugin/loops-1704117562175x705056703666192400) and select your app from the "Install in an application..." dropdown.

<img alt="Installing the Loops plugin from Bubble" />

Alternatively, go to the Plugins page in your Bubble admin, click "Add plugins" in the top right, and search for "Loops".

<img alt="Searching for the Loops plugin in Bubble" />

## Set up the plugin

To use the plugin you will need to add a Loops API key into Bubble.

First, go to your [Loops API settings page](https://app.loops.so/settings?page=api) and copy an API key. You may need to create a key first.

Then go to the Plugins page in Bubble, select the Loops plugin and paste your API key into the "API key" field, prepended with the word "Bearer" and a space.

<img alt="Adding a Loops API key to the Bubble plugin settings" />

## Using the plugin actions

To use the plugin actions in a workflow, select "Plugins" in the menu and you will see the options show up (prefaced with "Loops - ").

<img alt="Selecting a Loops action in a Bubble workflow" />

Here's a simple example of using the Bubble plugin to add all new users to your Loops audience.

After your sign up action, add a new "Loops - Create a contact" action.

In the form, add your user email into the "Email" field and user ID into the "User ID" field.

<img alt="Example Bubble workflow action to create a Loops contact" />

And that's it!

## Actions

The plugin contains actions that replicate what's possible with the [Loops API](/docs/api-reference/intro).

### Create a contact

The **Create a contact** action will create new contacts in Loops using the email address and user ID that you send from Bubble.

If you want to "create or update" users, use the [Update a contact](#update-a-contact) action instead.

You need to provide both email and user ID values, otherwise the underlying API request from Bubble will fail.

The **Create a contact with name** action lets you also send a first and last name to Loops.

[API documentation](/docs/api-reference/create-contact)

### Find a contact

The **Find contact by email** action will find a Loops contact based on the provided email address. This can be used to see if one of your user already exists in your Loops audience.

Similarly the **Find contact by user ID** action will find a contact by their ID.

[API documentation](/docs/api-reference/find-contact)

### Update a contact

This action will update the email address of a Loops contact who has the provided user ID.

This action can also be used to "update or create" contacts. If a contact doesn't already exist with the provided email or user ID, a contact will be created.

The **Update a contact with name** action lets you also send a first and last name to Loops.

[API documentation](/docs/api-reference/update-contact)

### Delete a contact

The actions **Delete a contact by email** and **Delete a contact by user ID** will delete a contact from Loops.

This is useful for when a user closes their account in your application and therefore you no longer want to email them from Loops.

[API documentation](/docs/api-reference/delete-contact)

### Send an event

The send event actions can be used to [trigger workflows](/docs/workflows) from your application. For example, you may have a welcome workflow which sends an email drip campaign to new users.

For the Send an event action, you need to provide an "Event name" value and the user's ID or email address.

<img alt="Bubble workflow action fields for sending an event to Loops" />

[API documentation](/docs/api-reference/send-event)

### Send transactional email

Transactional emails are one-time emails like password resets sent by apps to users based on an action.

To send transactional emails, you will first need to [create the email in Loops](/docs/transactional#compose-your-email).

Once you have written the email and added data variables, you can click **Next** to see the example payload for the API.

Note the names of the `dataVariables` (which you added in the email) because you need these in Bubble.

<img alt="Bubble plugin action fields for sending transactional email" />

Once you create the action in Bubble, you will see the three fields in the form.

Here is an example API payload for the Loops API:

```json theme={"dark"}
{
  "transactionalId": "clomzp89u635xl30px7wrl0ri",
  "email": "test@example.com",
  "dataVariables": {
    "lastLoggedIn": "20240214T10:01:29Z",
    "plan": "Pro"
  }
}
```

To get the same effect in Bubble, we need to enter the following into the "Data variables" field:

`"lastLoggedIn": "20240214T10:01:29Z", "plan": "Pro"`

To add user data into this field, place your cursor and select **Insert dynamic data** (see image above).

[API documentation](/docs/api-reference/send-transactional-email)


# Carrd integration
Source: https://loops.so/docs/integrations/carrd

Enable sign ups to Loops using a native Carrd form.

### Add a form to your Carrd site

<img alt="carrd image" />

1. Add a form to your site and select the **Custom** option.
2. Select **Send to URL** and paste in your form endpoint from the [Forms page in Loops](https://app.loops.so/forms).
   <img alt="form endpoint" />
3. Paste the form endpoint you copied from into the **URL** input in Carrd.
4. Change the **Method** to "AJAX" and the **Format** to "JSON".

<Tip>
  Our form submission endpoint has rate limiting, so you will see an error in testing if you
  submit more than once per minute or submit the same email twice.
</Tip>

### Customizing the form

In addition to collecting the email address, you can also collect any other contact property you want.

1. Follow Carrd's documentation to [add a new field to your form](https://carrd.co/docs/forms/setting-up-a-custom-form)
2. Determine your preference:
   * A hidden field that is set to a static value
   * A value that your user can set
3. Assign the field an "ID" matching the Loops API property name that you want to set. You can check the full list of your available properties from your [API Settings](https://app.loops.so/settings?page=api) page.

For example, if you want to set the User Group property, you would add a hidden field and set the ID to `userGroup`.

To add subscribers to specific [mailing lists](/docs/contacts/mailing-lists), add a field with an ID `mailingLists`. The **Value** can be a single mailing list ID or if you want to add subscribers to multiple lists, a comma-separated list of IDs.

<Warning>
  Mailing lists need to be marked **Public** in your [Lists settings](https://app.loops.so/settings?page=lists) in order for them to work in forms.
</Warning>


# Clay integration
Source: https://loops.so/docs/integrations/clay

Learn how to sync data between Clay and Loops.

Clay is a platform for managing and enriching your customer and user data. You can sync contact data between Clay and Loops using webhooks and API requests, as well as trigger emails from Clay.

## Send Loops contacts to Clay

You can send Loops contact data to Clay using Loops [webhooks](/docs/webhooks).

### Create a webhook in Clay

In Clay, add a Webhook source by clicking **More sources...** in the sidebar and selecting **Webhook**.

<img alt="Add a webhook in Clay" />

This generates a new webhook URL.

Make sure to change the **Send response as** option just below the URL to "JSON" to match the data format sent by Loops.

<img alt="Webhook setup in Clay" />

### Set up the webhook in Loops

Copy the URL and paste it into the **Endpoint URL** field on the [Webhooks](https://app.loops.so/settings?page=webhooks) settings page in Loops.

Activate `contact.created` events. This is the only event that makes sense to sync to Clay because it's the only [event type](/docs/webhooks#event-types) that contains a full contact record.

<img alt="Webhook in Loops" />

### Create a data mapping in Clay

Now, in order to create data mappings in Clay, you need to send some data from Loops. To do this we need to create a contact, which will trigger a webhook to be sent to Clay.

In your Loops [Audience page](https://app.loops.so/audience) you can create a new contact from the `+` button in the top right, or use the [API](/docs/api-reference/create-contact) or an [integration](/docs/integrations).

Once the contact is created, go back to Clay. You should see a webhook record in your table, and there should now be data shown in the **Setup mapping** section on the right.

<img alt="Event received in Clay" />

Now you can map data from Loops to columns in Clay. Click on the cell in the **Webhook** column. On the right you can click on attributes and map them to columns. Expand the **Contact** object to view the full record from Loops.

Click on an attribute you want to sync and then **Add as column**. You have the option to map the data to a new or existing column.

<img alt="Mapping data in Clay" />

Once you've mapped your desired data to Clay, future webhooks will automatically sync the same Loops data to your Clay table. You can test this by adding another contact to your Loops audience.

If you ever want to check or view the data coming into Clay, clicking on cells in the **Webhook** column will show the full request body for each request.

## Send Clay contacts to Loops

You can sync data to your Loops audience from Clay using a custom enrichment in your tables. This sends contact data to Loops whenever a row is created or updated (or on a manual schedule).

### Create a connection to the Loops API

Add an API connection to your table in Clay by selecting **Add enrichment** and searching for **HTTP API** as the data source.

In the **Account** section of the sidebar, click **+ Add account** or select an existing connection.

<img alt="Add a connection" />

If you're adding a new connection, give the connection a name (e.g. "Loops") and then add the two API Request Headers listed below.

You'll need to generate or copy an API key from [Settings -> API](https://app.loops.so/settings?page=api) in Loops and replace `<api_key>` with the key.

| Key           | Value              |
| :------------ | :----------------- |
| Authorization | `Bearer <api_key>` |
| Content-Type  | `application/json` |

Click **Save** to create the connection.

<img alt="Connection details" />

<Tip>
  If you want to edit the connection in the future, you can do so from **Settings -> Connections** in Clay.
</Tip>

### Create an API request

We're going to set up an "update contact" API request, which will *create or update* contacts in Loops.

In the **Configure** section, select "PUT" from **Method** dropdown and enter the following URL into the **Endpoint** field:

```
https://app.loops.so/api/v1/contacts/update
```

In the **Body** field, you can build up the request, using data from your table. Type `/` to add columns, and make sure to wrap values in quotes to create valid JSON data for the API.

<Warning>
  You must include an `email` in the request body.
</Warning>

<img alt="API request body" />

Data will be sent to Loops automatically when a row is created or updated when the **Auto-update** option is enabled. Toggle this off to only sync data manually.

<Tip>
  To edit the request in the future, click on the **HTTP API** column header and select **Edit column**.
</Tip>

<CardGroup>
  <Card title="Update contacts" href="/api-reference/update-contact" icon="user-pen">
    Read the API documentation for updating contacts
  </Card>
</CardGroup>

### Test the setup

To test the connection and request you created, click **Save** and then **Save and run (x) rows in this view**.

If the sync succeeds, you'll see the cell populate with "200", which means a successful API request was made.

<img alt="200 means success" />

If there is an error sending data to Loops, you can click on the cell in the **HTTP API** column to see the error message returned by the API, including the reason for the failure.

## Send events from Clay

You can trigger [workflows](/docs/workflows) from inside Clay by [sending events](/docs/events) with the Loops API.

### Create an event and a workflow

An [event](/docs/events) allows you to start a workflow when something happens in an external platform (like Clay). Workflows can contain emails, timers, and filters.

<CardGroup>
  <Card title="Events" href="/events" icon="bolt">
    Learn more about events
  </Card>

  <Card title="Workflows" href="/workflows" icon="arrows-rotate">
    Learn more about workflows
  </Card>
</CardGroup>

To define your event, go to [Settings -> Events](https://app.loops.so/settings?page=events) and click **Create**. You can specify [event properties](/docs/events/properties) for the event, which are data about specific events that you can add into your emails to personalize them for each recipient.

<img alt="Create an event" />

When you're done, go to the [Workflows page](https://app.loops.so/workflows) to create a new workflow. Use the "Event received" trigger and select your event from the previous step in the inspector panel on the right-hand side.

In this workflow, add as many emails as you want plus timers to space them out. You can personalize emails by [adding event properties](/docs/events/properties#using-event-properties-in-emails) into your emails body, subject and other sending settings fields.

<img alt="Workflow email" />

### Create an API request

To send an event with the API from Clay, we need to make a request containing our event data. If you've added event properties to your emails, we need to include those in the request.

Add a new enrichment to your table in Clay by selecting **Add enrichment** and searching for **HTTP API** as the data source. Then create or select an existing connection to the Loops API. Follow the [steps outlined above](#create-a-connection-to-the-loops-api).

In the **Configure** section of your HTTP API enrichment, select "POST" from **Method** dropdown and enter the following URL into the **Endpoint** field:

```
https://app.loops.so/api/v1/events/send
```

In the **Body** field, you can build up the request, using data from your table. Type `/` to add columns, and make sure to wrap values in quotes to create valid JSON data for the API.

<Warning>
  You must include an `eventName` and an `email`/`userId` in the request body. [Read more](/docs/api-reference/send-event#body)
</Warning>

<img alt="API request body" />

<Tip>
  You can also update contact information in Loops in the same request by including contact properties alongside `email` in the root of the request body. [More info](/docs/api-reference/send-event#contact-properties)
</Tip>

Events will be sent to Loops automatically when a row is created or updated with the **Auto-update** option enabled. Toggle this off to only sync data manually.

You can use the **Only run if** option to only send events when a certain condition is met.

<img alt="Conditional sending" />

<Tip>
  To edit the request in the future, click on the **HTTP API** column header and select **Edit column**.
</Tip>

<CardGroup>
  <Card title="Send event" href="/api-reference/send-event" icon="bolt">
    Read the API documentation for sending events
  </Card>
</CardGroup>

### Test the setup

Review the [information in the previous section](#test-the-setup) to see how to test your setup


# Clerk integration
Source: https://loops.so/docs/integrations/clerk

Sync Clerk users to Loops contacts and trigger workflows from Clerk authentication events like signup, login, and user deletion.

<Info>
  Our Clerk integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

Our Clerk integration is built on top of our [Incoming webhooks](/docs/integrations/incoming-webhooks) feature. This system lets you send webhooks from supported platforms directly to Loops so you can easily sync users and customers as well as send automated emails.

With Clerk, you can keep your user data synced to Loops so you can send emails to your userbase.

## Supported events

We accept the following Clerk events:

* `user.created`
* `user.deleted`
* `user.updated`
* `waitlistEntry.created`
* `waitlistEntry.updated`

[Clerk webhook docs](https://clerk.com/docs/webhooks/overview)

If you send other events, they will be ignored.

<Info>
  If you would like to see more events supported, please let us know by sending
  a message to [our support team](https://app.loops.so/settings?page=support). Please keep in mind only
  events that contain an email address are able to be processed.
</Info>

## Synced data

For `user.created` or `user.updated` events, we sync the following Clerk data to your Loops contacts:

* User ID
* Email address
* First name (optional)
* Last name (optional)

We use the IDs of Clerk users to match contacts in your Loops audience. If the user ID is not found in Loops, we will create a new contact.

The `user.deleted` event can be used to delete or unsubscribe your Clerk users from your Loops audience.

For `waitlistEntry.*` events, we sync:

* Email address

There is no user ID available for waitlist events. When a user graduates from your waitlist to being a user (i.e. you accept them and they complete their account), their user ID will be added to your contact in Loops.

## Create a webhook endpoint in Loops

[Follow the instructions here](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks) to create a new webhook endpoint, which will allow you to send webhook events directly to Loops.

<img alt="Endpoint form" />

## Create a webhook in Clerk

Next, you need to set up webhooks in Clerk.

Inside a project in Clerk, go to **Configure -> Webhooks** and click **+ Add Endpoint**. Paste in the endpoint URL from Loops, then select the event(s) you want to send (see our [supported events](#supported-events) above).

<img alt="Adding a webhook in Clerk" />

Click **Create** to finish.

The last step is to copy the signing secret into Loops. On the webhook page in Clerk, click the eye icon on the right to show the secret in the page. Copy the secret and paste it into the **Signing Secret** field in Loops.

<img alt="Reveal Clerk secret" />

Now you're all set up.

## Testing Clerk webhooks

Clerk offers a nice way to test webhooks. Click through to the webhook you created and then the **Testing** tab.

Select `user.created` or `user.updated` from the **Send event** dropdown, tweak the example data that's shown, then click **Send Example**.

This will send real data to your Loops account.

You can also test your webhook by creating and editing users in the **Users** page in Clerk, or by signing up in your application.

You can see all sent webhooks by going to **Webhooks**, clicking on the webhook and scrolling down to the **Message Attempts** section.

On Loops' end, you will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page.

## Examples

Here are some examples of how you can send data from Clerk to Loops to sync contacts and trigger useful emails to your customers.

### Syncing users to Loops

Create or update contacts in your Loops audience when a user is created or updated in Clerk.

1. Create a new Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks)).
2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.created` and `user.updated` events and paste in your endpoint's URL.
3. In Loops, make sure `user.created` and `user.updated` are toggled on on the Clerk settings page.

### Syncing waitlist signups to Loops

Create or update contacts in your Loops audience when someone joins your waitlist in Clerk.

1. Create a new Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks)).
2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `waitlistEntry.created` event and paste in your endpoint's URL.
3. In Loops, make sure `waitlistEntry.created` is toggled on on the Clerk settings page.
4. To keep track of waitlist users you can use the **Assign a user group** option. Enter something like "Waitlist" into the field.

### Send an email to all new Clerk users

Send an email from Loops when a new user is created in Clerk.

1. Set up your Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks)).
2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.created` event and paste in your endpoint's URL.
3. In Loops, make sure `user.created` is toggled on on the Clerk settings page.
4. Create a new workflow in Loops using our **Onboarding drip** template.
5. For the workflow trigger, select **Event received** and then select **Clerk** from the first dropdown and **user.created** from the second dropdown.

### Enter all new Clerk users into an onboarding email sequence

Send an email from Loops when a new customer is created in Clerk.

1. Complete Steps 1–5 from "Send an email to all new Clerk users" above.
2. Add more emails and timers into the workflow to complete your email sequence.

### Send an email to a deleted Clerk user

Send an email from Loops when a user is deleted in Clerk.

1. Set up your Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks)).
2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.deleted` event and paste in your endpoint's URL.
3. In Loops, make sure `user.deleted` is toggled on on the Clerk settings page.
4. Create a new workflow in Loops.
5. For the workflow trigger, select **Event received** and then select **Clerk** from the first dropdown and **user.deleted** from the second dropdown.
6. Add an email to the workflow.
7. You can optionally delete or unsubscribe the user from your audience from the Clerk settings in Loops. Note that both options will stop that contact from receiving future emails from Loops.


# Fivetran integration
Source: https://loops.so/docs/integrations/fivetran

Send user data from your data warehouse to Loops via Fivetran.

<Tip>
  This integration partner was previously called Census.
</Tip>

<Info>
  Our Fivetran integration lets you:

  * Create and update contacts
  * Trigger workflows when contacts are created or updated
</Info>

Loops is a partnered destination for Fivetran. We support syncing data via `userId` or `email`.

<Warning>
  New contacts cannot be created if no email is provided.
</Warning>

Within the Fivetran app, go to **Destinations** and click **+ New Destination**.

Select the **Loops** destination.

<img alt="Add Loops destination" />

You will be prompted to enter your Loops API Token, which you can find on your [API settings page](https://app.loops.so/settings?page=api), and to decide if you want to trigger workflows when contacts are created or updated.

<img alt="Create a destination" />

Click **Connect** and test out your destination.

Loops will now be available as a destination in Fivetran.

<img alt="Use the Loops destination" />

When choosing a sync behavior, you can choose either **Update or Create** or **Mirror**.

<img alt="Choose sync behavior" />


# Framer integration
Source: https://loops.so/docs/integrations/framer

Enable signups from your Framer site using an in-built or custom Loops component.

Collect new subscribers from your Framer site. There are a few ways to set this up.

1. [Framer Form component](#framer-form-component)\
   This uses the native Form component in Framer and gives good flexibility for extra form fields.
2. [Framer Loops component](#framer-loops-component)\
   This simplest method. A simple drop-in form with an email address input.
3. [Custom component with code](#advanced-integration)\
   The most flexible but complex option, using custom code.

<Tip>
  Our form submission endpoint has rate limiting, so you will see an error in
  testing if you submit more than once per minute or submit the same email
  twice.
</Tip>

## Framer Form component

Use [Framer Forms](https://www.framer.com/features/forms/) to easily create a form on your site.

### Insert the Form Component

From **Insert -> Forms** drag the **Form builder** component into your page. This will add an example form.

<img alt="Framer Form component" />

### Edit the form fields

Edit the fields in the form to match the data you want to collect from new subscribers.

An email field is required. Make sure to toggle the **Required** option to **Yes** for your email field.

You need to edit the **Name** value of each field to match the [contact properties' "API name"](/docs/contacts/properties) in Loops.

For example, the **Name** value must be "email" for the email address field.

<img alt="Email form field" />

You can add hidden fields to populate data like `mailingLists`, `userGroup` and `source` in Loops yet ensure they don't show up in the form.

You may have to click the `+` button to show the **Value** and **Hidden** options.

<Tip>
  To add subscribers to specific [mailing lists](/docs/contacts/mailing-lists), add a
  field with **Name** `mailingLists`. The **Value** can be a single mailing list
  ID or a comma-separated list of IDs.
</Tip>

<Warning>
  Mailing lists need to be marked **Public** in your [Lists
  settings](https://app.loops.so/settings?page=lists) in order for them to work
  in forms.
</Warning>

<img alt="Hidden form field" />

### Configure the form

The final step is to set the endpoint to your Loops form URL.

1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account.
2. Click on the **Settings** tab.
3. Copy the **Form Endpoint**.
   <img alt="Form endpoint" />
4. Back in Framer, select your form. In the **Send To** option, select "Webhook". Then paste the URL from Loops into the **API** field.
   <img alt="Framer form webhook" />

Now your form is all set up and can start receiving new subscribers.

### Set up a confirmation message

By default a "Thank you" message is shown inside the form button when the form is submitted successfully.

You can opt to use a redirect instead. You can add a confirmation message on another web page and use the **Redirect** option in the form settings.

## Framer Loops component

Framer has a built-in Loops option for creating simple signup forms with an email address field.

### Insert the Loops Component

From **Insert -> Forms** drag the **Loops** component into your page. This will add an example form.

<img alt="Framer Loops component" />

### Configure the form

Next, you need to add your Loops form ID to the **ID** field.

1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account.
2. Click on the **Settings** tab.
3. Copy the **Form ID**.
   <img alt="Form ID" />
4. Paste this ID into the **ID** field in the Framer component.
   <img alt="Loops form ID" />
5. Framer offers two extra fields in the component:
   * **User Group** populates the contact's `userGroup` value in Loops.
   * **Mailing list** can be used to subscribe contacts to your [mailing lists](/docs/contacts/mailing-lists). Add here one or more mailing list IDs separated by commas.
     <Warning>
       Mailing lists need to be marked **Public** in your [Lists
       settings](https://app.loops.so/settings?page=lists) in order for them to
       work in forms.
     </Warning>

### Set up a confirmation message

Make sure to also set up a confirmation message by clicking on the **Success** dropdown.

You can choose to show a message in an overlay with the "Show Overlay" option or redirect the user to another web page with the "Open Link" option.

To add an overlay message, click on the **Overlays** section header in the right-hand panel, choose between "Relative" and "Fixed" and make sure the **Show On** selection is "Submit".

## Advanced integration

This option adds a custom component into your Framer site [using form code](/docs/forms/custom-form) generated by Loops

### Generate the form code

1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account.
2. Click on the **Settings** tab.
3. Select “JSX” from the **Generate Form Code** dropdown (1), then copy the code snippet (2).

   <img alt="Generating JSX form code in Loops" />

### Embed the component in Framer

1. Create a new component. Toggle over to **Assets** in the Framer side panel then click the `+` button.

   <img alt="Creating a new Framer component from the Assets panel" />

2. Give your New Component a title.

   <img alt="Naming the new component in Framer" />

3. Finally, paste the code copied in from Loops into the code editor. You should see the Preview on the right fill in with a preview of your component.

   <img alt="Pasting Loops form code into the Framer code editor" />

4. Drag and drop your new asset anywhere on your page to use it :)


# Incoming webhooks
Source: https://loops.so/docs/integrations/incoming-webhooks

Send data to Loops from supported platforms using webhooks.

<Info>
  Incoming webhooks allow you to:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

This feature lets external platforms send webhook events directly to Loops, making it straightforward to create or update contacts in Loops automatically when changes happen in other platforms.

You can also trigger events when webhooks arrive in Loops so you can send automated email after something happens in your other accounts.

## How it works

External platforms send webhook events to Loops and Loops processes them to create and update contacts and trigger workflows.

For some platforms, you will need to manually create a webhook in the external platform, and for some it can be done via a simpler OAuth flow.

Note: we only process specific webhook events listed for each provider (and which contain an email address).

We return helpful messages in responses if there is an issue processing a webhook event. Check the webhook logs in your external platforms for more details.

For each type of webhook event, you can sync customer data like names and assign user groups, as well as trigger workflows via [events](/docs/events).

<img alt="Incoming webhook configuration" />

### Syncing contacts

The primary use case for incoming webhooks is to create and update contacts in your Loops audience. When data arrives in Loops, we grab the email address to create and update contacts in your Loops audience. To this end, we only support incoming events that contain an email address.

You can choose to update first and last name data from the webhook event as well.

Additionally, you can assign a user group value to each new contact, which helps create segments from webhook-created contacts.

Any new contact created via a webhook will have a source like "Stripe webhook" so you know where it originated from.

For events that reference record deletion, like Stripe's `customer.deleted` event, you can choose to unsubscribe or delete contacts in Loops.

### Subscribing to mailing lists

You can subscribe contacts to [mailing lists](/docs/contacts/mailing-lists) when they are created or updated via a webhook.

### Sending emails

Incoming webhooks can trigger emails if you connect events to [workflow triggers](/docs/workflows/triggers). This can be useful if you want to automatically send emails when something has happened in the external platform, for example a successful payment in Stripe or a new sign up in Clerk.

Just create a workflow using the **Event received** trigger and select the event you want to trigger on.

<img alt="Event received trigger" />

You can also trigger custom events from incoming webhooks if you specify an event in the **Trigger an additional custom event** section in the configuration.

## Set up incoming webhooks

To start sending webhook events to Loops, go to your chosen [integration's settings page](https://app.loops.so/settings) in Loops.

### Stripe

For Stripe, install the Loops app, which lets you skip the creation of webhooks as it's handled for you. Everything is set up for you automatically.

Click the **Connect to Stripe** button to install the Loops app.

### Polar

For Polar, sign in to Polar by clicking the **Sign in to Polar** button. This will create a new webhook endpoint in Polar so there is no manual set up required.

Select the Polar organisation you want to connect to. You will be redirected back to Loops when you are done.

### Clerk and Supabase

A webhook endpoint will be created for you. Copy the endpoint URL and paste it into your external platform.

<img alt="Endpoint form" />

You will need to also copy-paste a signing secret between the platform and Loops for extra security. There are detailed instructions for how to do this in the Loops settings page.

## Supported platforms

Learn more about how to configure webhooks for each platform, and see which events we support, below.

<CardGroup>
  <Card
    title="Clerk"
    icon={
  <svg viewBox="0 0 18 18" fill="none" aria-hidden="true"><ellipse cx="8.99999" cy="9" rx="2.81249" ry="2.8125" fill="#FF4A00"></ellipse><path fill="#FF4A00" d="M14.0674 15.6591C14.3067 15.8984 14.2827 16.2945 14.0015 16.4829C12.571 17.4411 10.8504 17.9999 8.9993 17.9999C7.14818 17.9999 5.42758 17.4411 3.99704 16.4829C3.71589 16.2945 3.69186 15.8984 3.93115 15.6591L5.98648 13.6037C6.17225 13.418 6.46042 13.3886 6.69424 13.5084C7.3856 13.8626 8.16911 14.0624 8.9993 14.0624C9.82948 14.0624 10.613 13.8626 11.3044 13.5084C11.5382 13.3886 11.8263 13.418 12.0121 13.6037L14.0674 15.6591Z"></path><path fill="#FF4A00" d="M14.0022 1.51706C14.2834 1.70539 14.3074 2.10155 14.0681 2.34084L12.0128 4.39619C11.827 4.58195 11.5388 4.61129 11.305 4.49151C10.6136 4.13733 9.83014 3.9375 8.99996 3.9375C6.20403 3.9375 3.93748 6.20406 3.93748 9C3.93748 9.83018 4.13731 10.6137 4.49149 11.3051C4.61127 11.5389 4.58193 11.827 4.39617 12.0128L2.34083 14.0682C2.10154 14.3074 1.70538 14.2834 1.51705 14.0023C0.558857 12.5717 0 10.8511 0 9C0 4.02944 4.02942 0 8.99996 0C10.8511 0 12.5717 0.55886 14.0022 1.51706Z" fillOpacity="0.5"></path></svg>
}
    href="/integrations/clerk"
  />

  <Card
    title="Polar"
    icon={<svg
  viewBox="-0.5 -0.5 16 16"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
  aria-hidden="true"
  height="24"
  width="24"
>
  <path
    d="M7.5 14.337187499999999C3.7239375000000003 14.337187499999999 0.6628125 11.276062499999998 0.6628125 7.5 0.6628125 3.7239375000000003 3.7239375000000003 0.6628125 7.5 0.6628125c3.7760624999999997 0 6.837187500000001 3.061125 6.837187500000001 6.837187500000001 0 3.7760624999999997 -3.061125 6.837187500000001 -6.837187500000001 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M7.5 14.337187499999999c-1.5104375 0 -2.7348749999999997 -3.061125 -2.7348749999999997 -6.837187500000001C4.765125 3.7239375000000003 5.9895625 0.6628125 7.5 0.6628125c1.5103749999999998 0 2.7348749999999997 3.061125 2.7348749999999997 6.837187500000001 0 3.7760624999999997 -1.2245 6.837187500000001 -2.7348749999999997 6.837187500000001Z"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M5.4488125 13.653500000000001c-2.051125 -0.6837500000000001 -2.7348749999999997 -3.6845624999999997 -2.7348749999999997 -5.811625 0 -2.1270625 1.025625 -4.7860625 3.418625 -6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
  <path
    d="M9.551187500000001 1.3464999999999998c2.051125 0.6837500000000001 2.7348749999999997 3.6846250000000005 2.7348749999999997 5.811625 0 2.1270625 -1.025625 4.7860625 -3.418625 6.495375"
    stroke="#FF4A00"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="1"
  ></path>
</svg>}
    href="/integrations/polar"
  />

  <Card title="Stripe" icon="stripe-s" href="/integrations/stripe" />

  <Card title="Supabase" icon={SupabaseIcon()} href="/integrations/supabase" />
</CardGroup>


# Integrately
Source: https://loops.so/docs/integrations/integrately

Connect Loops to over a thousand apps.

<Info>
  Our Integrately integration lets you:

  * Create, find, update and delete contacts
  * Send events to trigger workflows
  * Send transactional email
</Info>

## Create an automation

In Integrately, create an automation by searching for and selecting the two platforms you want to connect.

<img alt="Add an automation in Integrately" />

You'll need to connect to or sign in to each platform. When it comes to connecting to Loops, you need to create or copy an API key from [your API Settings page](https://app.loops.so/settings?page=api). Paste your API key into Integrately.

<img alt="Add Loops API key" />

Now you are connected to Loops and can continue setting up the automation.

## Manage contacts

To sync contacts, choose either the "Create contact" or "Create or update contact" action.

You can map data between the two platforms in the automation form. In this example, the "Email" field contains the data from the "Email" column in Google Sheets.

<img alt="Create a contact automation" />

## Send event

To send events from Integrately (i.e. to trigger a [workflow](/docs/workflows)), choose the "Send event" action and then map an "Email" and "Event name" in the available fields.

<img alt="Send event automation" />

## Send transactional email

To send transactional email from Integrately, select the "Send transactional email" action.

Map the email address you want to send to. Then add the ID of your transactional email from Loops (found on the "Publish" page of the transactional email editor).

Finally, add your email's data variables. You can easily map dynamic variables from the source app or add static text in the form.

<img alt="Send transactional email automation" />


# Make integration
Source: https://loops.so/docs/integrations/make

Utilize our official Make integration to manage contacts and send email.

<Info>
  Our Make integration lets you:

  * Create, find, update and delete contacts
  * Send events to trigger workflows
  * Send transactional email
</Info>

## Create a connection

Before you get started, head over to the API page and [create a new API key](https://app.loops.so/settings?page=api) in Loops. You'll need to copy the API key and paste it into Make.

To get started create a new scenario and select the "Loops" module.

<img alt="Adding loops in Make" />

Then select the "Create Connection" button and paste in your API key in the following screen.

<img alt="Create a connection with Make" />

After pasting your API key, click the "Save" button.

<img alt="Connect Loops API with Make" />

## Manage contacts and trigger workflows

There are a number of actions available to manage contacts and send emails.

<img alt="Adding a module with Make" />

## Send transactional email

There is a "Send transactional email" action you can use to send transactional emails. When setting up this action you can select an email from your Loops account, which will then display the required data variables in the form.

Alternatively you can use the "Make an API call" action.

<img alt="Set up transactional sending in Make" />

Make sure that the URL is `/v1/transactional` rather than the full endpoint URL.

You can copy an email's example payload from its Publish page in Loops by clicking the **Show payload** button. Paste it into the "Body" field in Make and select the data you want to send to Loops.

<Card title="Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
  View the API reference for sending transactional email.
</Card>

## Triggers

The Make integration contains a trigger that lets you listen for [Loops webhooks](/docs/webhooks), allowing you to do things in Make based on activity in your Loops account.

All of Loops' webhook events are available as triggers in Make.

<Info>
  Loops were renamed to Workflows on May 6, 2026. Make trigger bodies still use
  webhook `loop` names for compatibility, including `loop.email.sent`, `loopId`,
  `loopName`, and `sourceType: "loop"`.
</Info>

Add the **Watch Events** trigger to your Make scenario and use an existing or new Loops connection ([see above](#create-a-connection)).

<img alt="Make webhook settings" />

You will be prompted to copy the webhook URL provided by Make and [paste it into your Loops webhook settings](/docs/webhooks#set-up-webhooks).

Make sure to toggle on the **Immediately as data arrives** option in your scenario's bottom toolbar in Make so that the trigger fires every time a webhook event arrives.


# Novu integration
Source: https://loops.so/docs/integrations/novu

Send Novu email notifications with Loops SMTP.

Set up an SMTP connection to send Novu notification emails with Loops.

## Set up Loops SMTP in Novu

Go to the [Integration Store settings](https://dashboard-v2.novu.co/integrations) in Novu and create a new email provider.

Click **Connect Provider** and then **Custom SMTP**.

Give your provider a name like "Loops SMTP" in the **Name** field.

Then add the following details into each field:

| Field    | Value                                                                                       |
| -------- | ------------------------------------------------------------------------------------------- |
| User     | `loops`                                                                                     |
| Password | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops |
| Host     | `smtp.loops.so`                                                                             |
| Port     | `587`                                                                                       |

You also have to add values to the **From email address** and **Sender name** fields because Novu requires them. You can add any value here because Loops will overwrite these values when sending emails.

Click **Create Integration** to finish setup.

<img alt="Setting up custom SMTP in Novu" />

## Create Transactional emails in Loops

Next, create new transactional emails for the emails you are sending from Novu.

In Loops, go to the [Transactional page](https://app.loops.so/transactional) and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates).

<img alt="Creating a transactional email template in Loops" />

You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like.

You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails.

For each Loops template you create, you can add [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Novu to be inserted into each email.

Once you're done creating the email and adding the data variables, click **Next**. On the next page, click the **Show payload** button to view the API payload for your template. You will need this for the next step.

<img alt="Viewing the transactional email payload in Loops" />

Make sure to also publish your email! It won't send unless it's published.

<CardGroup>
  <Card title="Transactional email guide" icon="code" href="/transactional">
    Read our detailed guide for sending transactional emails.
  </Card>
</CardGroup>

## Configure email templates in Novu

The final setup step is to add email templates in Novu.

Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data.

In Novu, go to **Workflows** and create a new workflow. Give it a descriptive name and click **Create workflow**.

You will enter the workflow UI. Add an **Email** node. In the email body paste the transactional payload from Loops from the previous step.

<img alt="Novu template editor" />

Add a subject, but note that this will be overwritten by the subject you added to your Loops transactional email.

Next you need to add some Novu data into the template, namely the recipient's email and any data variables you added in Loops.

Here is an example **Confirm signup** email template. This payload was copied from the template's Publish page in Loops, then the `{{ subscriber.email }}` and `{{ payload.loginUrl }}` variables from Novu were added.

You can add any custom data to the `payload` object when triggering emails from Novu. We need to pass those same values to your Loops transactional email via the `dataVariables` data.

```json Email template in Novu theme={"dark"}
{
  "transactionalId": "cm67vfcgs00pha22s3qevs7nr",
  "email": "{{ subscriber.email }}",
  "dataVariables": {
    "loginUrl": "{{ payload.loginUrl }}"
  }
}
```

If you want to add each Novu subscriber to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ subscriber.email }}` value.

```json Email template in Novu {4} theme={"dark"}
{
  "transactionalId": "cm67vfcgs00pha22s3qevs7nr",
  "email": "{{ subscriber.email }}",
  "addToAudience": true,
  "dataVariables": {
    "loginUrl": "{{ payload.loginUrl }}"
  }
}
```

If you want to include [Novu subscriber attributes](https://docs.novu.co/concepts/subscribers#subscriber-attributes) to personalize your Loops email, you can add them into your Novu template just like the following example.

Make sure to add fallbacks for all non-required values and add the corresponding data variables (e.g. `firstName` here) into your Loops transactional email.

```json Email template in Novu {6} theme={"dark"}
{
  "transactionalId": "cm67vfcgs00pha22s3qevs7nr",
  "email": "{{ subscriber.email }}",
  "dataVariables": {
    "loginUrl": "{{ payload.loginUrl }}",
    "firstName": "{{ subscriber.firstName | default: 'subscriber' }}"
  }
}
```

Here's how a template with variables added looks in the Novu editor:

<img alt="Novu template editor" />

Now you're all set up to start sending!

## Trigger emails with Novu

When it comes to triggering Novu notifications to subscribers, you can use the Novu [SDKs](https://docs.novu.co/sdks/overview) or [API](https://docs.novu.co/api-reference/events/trigger-event).

In each call, you need to specify your workflow by its ID, add recipient data and also pass in any data variables for your Loops transactional email into the payload.

Using the same example from above, here's the trigger using Novu's Node SDK.

<Info>
  You can add a `to.email` value to send notifications to a specific email
  address.
</Info>

```javascript theme={"dark"}
import { Novu } from "@novu/node";

const novu = new Novu("<NOVU_SECRET_KEY>");

await novu.trigger("<WORKFLOW_TRIGGER_IDENTIFIER>", {
  to: {
    subscriberId: "67867a14722783d44d69fc5a",
  },
  payload: {
    loginUrl: "https://myapp.com/login/",
  },
});
```

Here's the same request using the API:

```json theme={"dark"}
POST https://api.novu.co/v1/events/trigger

{
  "name": "<WORKFLOW_TRIGGER_IDENTIFIER>",
  "to": {
    "subscriberId": "67867a14722783d44d69fc5a"
  },
  "payload": {
    "loginUrl": "https://myapp.com/login/"
  }
}
```

<Tip>
  To view all sends of your transactional emails, click through to the email
  from the [Transactional](https://app.loops.so/transactional) page in Loops,
  where you'll find the Metrics page containing a table showing all sends and
  some statistics.
</Tip>

## Testing the integration

Novu offers a testing UI where you can try out your set up before going live.

Go to the **Workflows** page and click on your workflow, then select the **Trigger** tab.

Here you will be able to set different subscriber and payload data and send test email notifications. The SDK examples below also update, so you can easily create code for your application.

You can also see logs of all emails sent from the [Activity Feed](https://dashboard-v2.novu.co/env/_/activity-feed) page in Novu.

<img alt="Novu testing" />

## Important notes

* The subject in Novu templates is always overwritten by the subject added to the corresponding template in Loops.
* The **From email address** and **Sender name** configured in your Novu SMTP settings are always overwritten by the sender details added to your templates in Loops.


# Polar
Source: https://loops.so/docs/integrations/polar

Sync contacts and send emails triggered by events in Polar.

<Info>
  Our Polar integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

Our Polar integration is built on top of our [Incoming webhooks](/docs/integrations/incoming-webhooks) feature. This system lets you send webhooks from supported platforms directly to Loops so you can easily sync users and customers as well as send automated emails.

You can sync your Polar customer data to Loops for customer, order and subscription events and trigger emails with [workflows](/docs/workflows).

## Supported events

We accept the following Polar events:

* `customer.created`
* `customer.updated`
* `customer.deleted`
* `order.paid`
* `subscription.created`
* `subscription.active`
* `subscription.canceled`
* `subscription.uncanceled`
* `subscription.past_due`
* `subscription.revoked`

<Note>
  When a new subscription is created, the following events are all sent by Polar:

  * `subscription.created`
  * `subscription.active`
  * `customer.created`
  * `order.paid`
</Note>

[Polar webhook docs](https://polar.sh/docs/integrate/webhooks)

If you send other events, they will be ignored.

<Info>
  If you would like to see more events supported, please let us know by sending
  an email to [help@loops.so](mailto:help@loops.so). Please keep in mind only
  events that contain an email address are able to be processed.
</Info>

## Synced data

We sync the following Polar data to your Loops contacts for every incoming event:

* Email address
* First and last name (optional)
* External ID (optional)

We use the email addresses of Polar customers to match contacts in your Loops audience. If the email address is not found in Loops, we will create a new contact.

The external ID is useful if you want to link your Loops contact record to unique users in your application via Polar's [external\_id](https://polar.sh/docs/api-reference/customers/create#body-external-id-one-of-0) field.

The `customer.deleted` event can be used to delete or unsubscribe your Polar customers from your Loops audience.

## Connect Loops and Polar

To get started, go to [Settings -> Polar](https://app.loops.so/settings?page=polar) in Loops and click the **Sign in to Polar** button. Select the Polar organisation you want to connect to. You will be redirected back to Loops when you are done.

A new webhook endpoint will be created in Polar, with the supported events enabled. You can check this by going to **Developers -> Webhooks** in Polar and then clicking on the "Loops" endpoint.

## Configure events

You can select and configure the Polar events you want to be sent to Loops.

For each event, you can choose to trigger workflows via [events](/docs/events) as well as update customer data like assigning a user group and syncing customer names.

Make sure to click **Save** to save your changes.

## Testing Polar webhooks

You can test a `customer.*` webhook by creating a new customer in Polar.

You can see all sent webhooks by going to **Developers -> Webhooks** and then clicking on an endpoint.

On Loops' end, You will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page.

## Examples

Here are some examples of how you can send data from Polar to Loops to sync contacts and trigger useful emails to your customers.

### Syncing customers to Loops

Create or update contacts in your Loops audience when a customer is created or updated in Polar.

1. Connect Loops to Polar.
2. In Loops, make sure `customer.created` and `customer.updated` are toggled on on the Polar settings page.
3. If you want to sync customer names, open the **Sync additional data** section and toggle on **Full name**.

### Send an email to all new Polar customers

Send an email from Loops when a new customer is created in Polar.

1. Create a new workflow in Loops using our **Stripe - New Customer** template.
2. For the workflow trigger, select **Event received** and then select **Polar** from the first dropdown and **customer.created** from the second dropdown.
3. Toggle on `customer.created` on the Polar settings page.

### Send an email after a successful order

Send an email from Loops when an order is paid in Polar.

1. Create a new workflow in Loops using our **Stripe - Payment Successful** template.
2. For the workflow trigger, select **Event received** and then select **Polar** from the first dropdown and **order.paid** from the second dropdown.
3. Toggle on `order.paid` on the Polar settings page.

### Subscription created email

Send an email from Loops when a new subscription is created in Polar.

1. Create a new workflow in Loops.
2. For the workflow trigger, select **Event received** and then select **Polar** from the first dropdown and **subscription.created** from the second dropdown.
3. Add an email node to the workflow and create your email.
4. Toggle on `subscription.created` on the Polar settings page.


# PostHog
Source: https://loops.so/docs/integrations/posthog

Send events and contacts data to Loops from PostHog.

PostHog's Loops integration lets you send events from your project to Loops, as well as add users to your Loops audience.

<Info>
  PostHog's [data pipelines](https://posthog.com/cdp) add-on or a self-hosted version of PostHog is required for this integration to work.
</Info>

Here's how to set up a Loops destination in PostHog.

## Create a Loops destination

In your PostHog dashboard go to **Data pipelines**, click **+ New destination** and search for "Loops".

You will see two options:

* Send events to Loops.so (use this to trigger [workflows](/docs/workflows))
* Update contacts in Loops.so (this will *create or update* contacts in your Loops audience)

<img alt="Search for Loops in PostHog" />

Choose the one you want and click **+ Create**.

## Add your API key

You need to add a Loops API key to each destination so that PostHog can send data to your Loops account.

In Loops, go to [Settings -> API](https://app.loops.so/settings?page=api) and generate or copy an API key.

Back in PostHog, paste the API key in the **Loops API Key** field.

<img alt="Add Loops API key" />

## Customize the destination

There are a few options available to customize the data that is sent to Loops for both destination types. This helps make sure that you're sending the correct data for your events and contacts.

### Send events

In the **Send event to Loops.so** destination, you can filter which events are sent to Loops from the **Filters** section. The default is "Pageview" (which is sent with the event name "\$pageview" to Loops).

<img alt="Send events destination" />

By default `pathname` is sent with the event as an [event property](/docs/events/properties), making it available in all emails triggered by the event.

If you want to add more event properties you can use the **Property mapping** option.

If you check the **Include all properties as attributes** option, all properties from the event will be sent to Loops.

### Update contacts

The **Update contacts in Loops.so** destination lets you sync users to Loops. If a matching contact doesn't already exist in Loops, one will be created.

In this destination you can filter which events are sent to Loops from the **Filters** section. The defaults are "Identify" and "Set person properties".

<img alt="Update contacts destination" />

By default `email`, `userId`, `firstName` and `lastName` are synced to Loops. To add more contact properties you can use the **Property mapping** option, which allows you to map specific person data to contact properties in Loops. You can see all available contact properties in your account from [Settings -> API](https://app.loops.so/settings?page=api). Make sure to use the "API name" when mapping values in PostHog.

If you check the **Include all properties as attributes** option, all properties from the person record in PostHog will be added to contacts in Loops. This may create a lot of custom contact properties in Loops, so we suggest running a mock test (see below) first to see which data would be sent.

### Mailing lists

To add contacts to your Loops [mailing lists](/docs/contacts/mailing-lists), you need to make a quick code change to the destination.

Click **Edit source code** on your destination's page.

For the **updating contacts** destination, edit lines 6–9 and add a new `mailingLists` section. Here, add your mailing list ID(s) and `true` (to subscribe the contact) or `false` (to unsubscribe).

```javascript {4-6} theme={"dark"}
let payload := {
  'email': inputs.email,
  'userId': person.id,
  'mailingLists': {
    '<your_list_id>': true
  }
}
```

<CardGroup>
  <Card title="Update contacts" href="/api-reference/update-contact" icon="user-pen">
    Read the API documentation for updating contacts in Loops.
  </Card>
</CardGroup>

For the send events destination, edit lines 6–11 and add a new `mailingLists` section. Here, add your mailing list ID(s) and `true` (to subscribe the contact) or `false` (to unsubscribe).

```javascript {6-8} theme={"dark"}
let payload := {
  'email': inputs.email,
  'userId': person.id,
  'eventName': event.event,
  'eventProperties': {},
  'mailingLists': {
    '<your_list_id>': true
  }
}
```

<CardGroup>
  <Card title="Send events" href="/api-reference/send-event" icon="bolt">
    Read the API documentation for sending events to Loops.
  </Card>
</CardGroup>

## Testing

To test if your configuration is working as expected, expand the **Testing** section at the bottom of the page.

<img alt="Testing the integration" />

You can send real test events to Loops by clicking **Test function**. Check [Settings -> Events](https://app.loops.so/settings?page=events) and your [Audience](https://app.loops.so/audience) pages in Loops to see if data is coming across as you expect.

If you'd rather not send actual data to Loops during testing, select the **Mock out HTTP requests** option. This will show the expected request data (including all contact and event properties) rather than send a request to Loops.


# RudderStack
Source: https://loops.so/docs/integrations/rudderstack

Connect Loops to hundreds of apps to manage contacts and send emails.

<Info>
  Our RudderStack integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

## Configuring the destination

In RudderStack, go to [Destinations](https://app.rudderstack.com/destinations) and click **New destination**. Search for "Loops" and select it.

<img alt="Adding Loops in RudderStack" />

Select the source you want to connect Loops to.

On the next page you need to add a Loops API key. You can generate a new one on the [Loops API Settings page](https://app.loops.so/settings?page=api) and paste it into the **API Key** field.

<img alt="Configuring the destination" />

Click **Continue** at the bottom of the page to finish the setup.

## Create or update a contact

To send contact data to Loops, use RudderStack's [`identify` call](https://www.rudderstack.com/docs/event-spec/standard-events/identify/).

Identify users with a unique user ID from your source. You can include contact properties in the traits object, like `firstName` and `lastName` in this example.

<Warning>
  For new contacts, make sure to include an email address, otherwise the call will fail. If the contact has already been sent to Loops with its user ID, you can omit the email address unless you want to update that value. In general, we recommend to always include an email address.
</Warning>

```javascript theme={"dark"}
rudderanalytics.identify(userId, {
  email: "test@example.com",
  firstName: "Adrian",
  lastName: "Brown"
});
```

To manage [mailing list](/docs/contacts/mailing-lists) subscriptions, add a `mailingLists` object to the traits object. The key is the ID of the mailing list and the value is a boolean indicating whether the contact should be subscribed or unsubscribed.

```javascript theme={"dark"}
rudderanalytics.identify(userId, {
  email: "test@example.com",
  firstName: "Adrian",
  lastName: "Brown",
  mailingLists: {
    cly2xnjqn002z0mmn68uog1wk: true,
  },
});
```

## Send an event

You can trigger emails from RudderStack by triggering [events](/docs/events) via [`track` calls](https://www.rudderstack.com/docs/event-spec/standard-events/track/).

You should add and define your events in [Settings -> Events](https://app.loops.so/settings?page=events) including any expected [event properties](/docs/events/properties).

The event name in your `track` call must match the name of the event in Loops. Data sent in the properties object will be sent as event properties to Loops, for use in your emails.

<Warning>
  Make sure to call `identify` before `track` so the event is associated with a specific contact.
</Warning>

```javascript theme={"dark"}
rudderanalytics.track("newUser", {
  plan: "Pro Annual",
  accountType: "Facebook"
});
```

## Testing

### In RudderStack

RudderStack includes features to help you test your integration.

First of all you can see all `identify` and `track` calls coming in from your sources. Click on a source and select the **Events tab**.

<img alt="Source events chart" />

Click on the **Live events** button in the top right to view details of events as they come in.

You can also see all of the calls being sent from RudderStack to your destinations. Click on a destination and then the **Events** tab. At the bottom of this page you will see a table showing events. Click on these to see any errors that have occurred when sending data to Loops.

<img alt="Destination events" />

### In Loops

You can verify contact updates have happened in Loops from the [Audience page](https://app.loops.so/audience), and you can see all incoming events from the [Events page](https://app.loops.so/settings?page=events).

Click on individual events in the **Event Log** table to view the payload that Loops processed.

<img alt="Events page" />


# Segment
Source: https://loops.so/docs/integrations/segment

Utilize our official Segment integration to manage contacts and send email.

<Info>
  Our Segment integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

Visit our [Segment integration](https://segment.com/catalog/integrations/actions-loops/) to learn more and follow the steps below.

## Configuring the destination

After opening the link above, click **Configure Loops (Actions)**.

<img alt="Adding Loops in Segment" />

Select your data source, give the destination a name, and click **Create destination**.

Next, you’ll need an API key. You can generate a new one for Segment on the [Loops API Settings page](https://app.loops.so/settings?page=api).

Enter the API key on your Segment destination settings:

<img alt="Add an API key" />

Enable the destination and click **Save Changes**. Note that no data will start flowing until you create specific mappings for Loops.

## Mappings

Segment action destinations require that you map specific fields from your source to your destination (in this case Loops). You can set this up by clicking into the **Mappings** tab and adding a new mapping. Currently we support updating contacts in Loops and sending events into Loops.

### Create or update contact

First, select which events to map. Typically for contact creation and updates, the most useful event to map will be "Identify".

<img alt="Map contact properties" />

The next step is to load a sample event to help you map fields appropriately.

Contact properties are found in the `traits` object.

For this example, we’ll be using this test event:

```json theme={"dark"}
{
  "messageId": "segment-test-message-gt3ds8",
  "timestamp": "2023-05-24T17:58:30.352Z",
  "type": "identify",
  "email": "test@example.com",
  "traits": {
    "firstName": "Adam",
    "favoriteColor": "blue",
    "favoriteNumber": 42
  },
  "userId": "test-user-a5h7xb"
}
```

<Warning>
  When sending a contact's details to Loops, you must include an Email and a User ID.
</Warning>

We've provided some defaults for the mappings, which will show up in the third step, but it is important you review them:

<img alt="Default contact mappings" />

#### Custom contact properties

Segment does not provide an interface to provide the names and types for [custom contact properties](/docs/contacts/properties#custom-contact-properties) that you might be using with Loops. In our example, those fields are `favoriteColor` and `favoriteNumber`.

You can pass contact properties as a dictionary in the **Custom Contact Attributes** field.

<Warning>
  Ensure that the keys and values you provide match the schema you’ve created in your [Contact properties settings](https://app.loops.so/settings?page=api).
</Warning>

Click **Edit Object** to specify the custom fields you want to send to Loops individually:

<img alt="Mapping custom contact properties" />

#### Subscribed

In most cases, you want to leave the **Subscribed** field as the default (deselected). Setting this to `true` will re-subscribe contacts who had previously unsubscribed, and setting it to `false` will unsubscribe contacts from receiving email. Leaving it deselected will default new users as subscribed to email and not update the email preference for existing contacts.

<img alt="Subscribed field" />

#### Mailing lists

To subscribe contacts to [mailing lists](/docs/contacts/mailing-lists) there are two options.

The first method is to manually edit the **Mailing Lists** data. This will allow you to enter list ID values that are the same for every contact.

Click the **Edit Object** option, then the **+ Add Mapping Field** button. In the **Select event variable** field enter "true" or "false" (to subscribe or unsubscribe) and in the **Enter key name** field enter your list ID(s).

You can add multiple lists by clicking on the **+ Add Mapping Field** button again. Make sure the data shown below the fields has the same structure as in the image below.

<img alt="Adding list IDs in the mapping UI" />

Another option is to add mailing list data to your Identify call, which will let you add more dynamic data for each contact. You can test this by adding a `mailingLists` object to `traits` in your test event with list IDs as keys and `true` (to subscribe) or `false` (to unsubscribe) as values.

```json theme={"dark"}
{
  "messageId": "segment-test-message-gt3ds8",
  "timestamp": "2023-05-24T17:58:30.352Z",
  "type": "identify",
  "email": "test@example.com",
  "traits": {
    "firstName": "Adam",
    "favoriteColor": "blue",
    "favoriteNumber": 42,
    "mailingLists": {
      "cm06f5v0e45nf0ml5754o9cix": true,
      "cm16k73gq014h0mmj5b6jdi9r": true,
    }
  },
  "userId": "test-user-a5h7xb"
}
```

Then you need to map the data. Click the **Select Object** option, then search for `traits.mailingLists` from the Event Variables options.

<img alt="Selecting the mailing list data from traits" />

#### Testing

After the mappings are configured you can preview the data that will be sent.

<img alt="Event data preview" />

You can also send a test event to Loops to verify everything is working (this will send actual data to your Loops account). If it works, you will get a successful response:

<img alt="Successful response" />

Check your [Loops audience page](https://app.loops.so/audience) to ensure the contact was created or updated as intended.

<Warning>
  Sending another test event with the same User ID or Email will update the existing contact instead of creating a new contact.
</Warning>

### Send event

You can send events to trigger [workflows](/docs/workflows).

First, select which event(s) to map. Typically for sending an event, the most useful event to map will be "Track".

We recommend that you filter the events down to only ones that you plan on using within Loops using the **Event Name** filter:

<img alt="Sent event in Segment" />

Then after defining or loading a sample event in step 2, configure the mapping.

<img alt="Sent event in Segment" />

#### Required fields

The **User ID** field is required. If a contact already exists in your Loops audience, the **Contact Email** field is optional; if the contact does not exist in Loops you need to include an email address so that a contact can be created and an email can be sent (we recommend always including an email address).

#### Event properties

If you want to add [event properties](/docs/events/properties), you can pass them as a dictionary to the **Event Properties** field. These properties can be included in emails triggered by your event.

You can choose to select the `properties` object to send all data from the track call. Or if you click **Edit Object** you can select individual properties. Make sure to enter the correct contact property name from Loops in the **Enter key name** field.

<img alt="Configure event mappings" />

#### Contact properties

You can update contacts at the same time as sending an event with the **Contact Properties** field.

Click **Edit Object** and specify the properties and values you want to update on your contacts.

<img alt="Mapping custom contact properties" />

<Warning>
  Ensure that the keys and values you provide match the schema you’ve created in your [Contact properties settings](https://app.loops.so/settings?page=api), otherwise some data may not be captured by Loops.
</Warning>

You can choose to add a contact's email address in the **Contact Email** field or in the **Contact Properties** object.

#### Testing

After configuring the mapping, you can send a test event at the bottom of the page (this will send actual data to your Loops account). You can preview the data that will be sent.

<img alt="Send event preview" />

The response should indicate success:

<img alt="Success message" />

You can verify the event was received on your [Events page](https://app.loops.so/settings?page=events) in Loops.

## Sending data to Segment

The following examples show how you can send data from your application to Segment for the two Loops actions.

The examples use Segment's [Analytics.js library](https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/), but the premise is similar for other libraries.

### Create or update contact

For this action you should send a `identify()` event. Add contact properties (including any [custom properties](/docs/contacts/properties#custom-contact-properties)) in the traits object:

```javascript theme={"dark"}
analytics.identify("97980cfea0067", {
  email: "test@example.com",
  plan: "premium",
  logins: 5
});
```

If you're mapping a `mailingLists` object from `traits` ([read more above](#mailing-lists)) add it like this:

```javascript theme={"dark"}
analytics.identify("97980cfea0067", {
  email: "test@example.com",
  plan: "premium",
  logins: 5,
  mailingLists: {
    "cm06f5v0e45nf0ml5754o9cix": true,
    "cm16k73gq014h0mmj5b6jdi9r": true,
  }
});
```

### Send event

For this action send a `track()` event. Data sent in the properties object will be sent as [event properties](/docs/events/properties) to Loops. Make sure that you have added these event properties in your [Events Settings](https://app.loops.so/settings?page=events) in Loops before sending the event to Segment.

```javascript theme={"dark"}
analytics.track("User Registered", {
  plan: "Pro Annual",
  accountType: "Facebook",
  firstName: "Phil"
});
```

You can update a contact's properties when sending events (for example, `firstName`) by mapping values in your properties within the **Contact Properties** object ([see above](#contact-properties)).


# Stripe integration
Source: https://loops.so/docs/integrations/stripe

Sync contacts and send emails triggered by events in Stripe.

<Info>
  Our Stripe integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

Our Stripe integration is built on top of [Incoming webhooks](/docs/integrations/incoming-webhooks) and runs via the Loops app for Stripe.

You can sync your Stripe customer data to Loops for customer and invoice-related events and trigger emails with [workflows](/docs/workflows).

## Supported events

We accept the following Stripe events:

* `charge.dispute.created`
* `charge.dispute.closed`
* `checkout.session.async_payment_succeeded`
* `checkout.session.async_payment_failed`
* `checkout.session.completed`
* `customer.created`
* `customer.updated`
* `customer.deleted`
* `customer.subscription.created`
* `customer.subscription.paused`
* `customer.subscription.resumed`
* `customer.subscription.trial_will_end`
* `customer.subscription.deleted`
* `invoice.paid`
* `invoice.payment_failed`
* `invoice.upcoming`
* `quote.finalized`
* `quote.accepted`
* `quote.canceled`

<Tip>
  If you cannot see some of these events in the Stripe settings page, please install Loops' Stripe app. Click **Connect to Stripe** at the top of the page.
</Tip>

[Stripe webhook docs](https://docs.stripe.com/webhooks)

If you send other events, they will be ignored.

<Info>
  If you would like to see more events supported, please let us know by sending
  a message to [our support team](https://app.loops.so/settings?page=support). Please keep in mind only
  events that contain an email address are able to be processed.
</Info>

## Synced data

We sync the following Stripe data to your Loops contacts for every incoming event:

* Email address
* First and last name (optional)

We use the email addresses of Stripe customers to match contacts in your Loops audience. If the email address is not found in Loops, we will create a new contact.

The `customer.deleted` event can be used to delete or unsubscribe your Stripe customers from your Loops audience.

## Install the Loops app in Stripe

To get started, install the Loops Stripe app from [Settings -> Stripe](https://app.loops.so/settings?page=stripe). Click **Connect to Stripe** and follow the installation steps provided by Stripe. You will be redirected back to Loops when you are done.

## Configure events

You can select and configure the Stripe events you want to be sent to Loops.

For each event, you can choose to trigger workflows via [events](/docs/events) as well as update customer data like assigning a user group and syncing customer names.

Make sure to click **Save** to save your changes.

## Testing Stripe webhooks

You can test a `customer.*` webhook by creating a new customer in the [Customers](https://dashboard.stripe.com/customers) page in Stripe.

You can also use the Stripe CLI tool to mimic events, by using the [`trigger` command](https://docs.stripe.com/cli/trigger).

You can see all sent webhooks by going to **Developers -> Webhooks** and then clicking on an endpoint.

On Loops' end, you will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page.

## Examples

Here are some examples of how you can send data from Stripe to Loops to sync contacts and trigger useful emails to your customers.

### Syncing customers to Loops

Create or update contacts in your Loops audience when a customer is created or updated in Stripe.

1. Install the Loops app in Stripe.
2. In Loops, make sure `customer.created` and `customer.updated` are toggled on on the Stripe settings page.
3. If you want to sync customer names, open the **Sync additional data** section and toggle on **Full name**.

### Send an email to all new Stripe customers

Send an email from Loops when a new customer is created in Stripe.

1. Create a new workflow in Loops using our **Stripe - New Customer** template.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **customer.created** from the second dropdown.
3. Toggle on `customer.created` on the Stripe settings page.

### Send an email to Stripe Checkout customers

Send an email from Loops when a customer pays via Stripe Checkout.

1. Create a new workflow in Loops using our **Stripe - Payment Successful** template.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **checkout.session.completed** from the second dropdown.
3. Toggle on `checkout.session.completed` on the Stripe settings page.

### Successful payment email

Send an email from Loops when an invoice is paid in Stripe.

1. Create a new workflow in Loops using our **Stripe - Payment Successful** template.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **invoice.paid** from the second dropdown.
3. Toggle on `invoice.paid` on the Stripe settings page.

### Failed payment email

Send an email from Loops when an invoice payment fails in Stripe.

1. Create a new workflow in Loops using our **Stripe - Payment Failed** template.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **invoice.payment\_failed** from the second dropdown.
3. Toggle on `invoice.payment_failed` on the Stripe settings page.

### Subscription created email

Send an email from Loops when a new subscription is created in Stripe.

1. Create a new workflow in Loops.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **customer.subscription.created** from the second dropdown.
3. Add an email node to the workflow and create your email.
4. Toggle on `customer.subscription.created` on the Stripe settings page.

### Quote accepted email

Send an email from Loops when a quote is accepted in Stripe.

1. Create a new workflow in Loops.
2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **quote.accepted** from the second dropdown.
3. Add an email node to the workflow and create your email.
4. Toggle on `quote.accepted` on the Stripe settings page.


# Supabase
Source: https://loops.so/docs/integrations/supabase

Send Supabase authentication emails and sync Supabase users with Loops.

Loops integrates with Supabase in two ways:

* sending Supabase Auth emails
* syncing Supabase users into your Loops audience.

Pick the guide that matches what you're setting up.

<CardGroup>
  <Card title="Auth Hook" icon={SupabaseIcon()} href="/integrations/supabase/send-email">
    Send every Supabase Auth email through Loops.
  </Card>

  <Card title="Contact sync" icon={SupabaseIcon()} href="/integrations/supabase/contact-sync">
    Sync Supabase users to your Loops audience and trigger workflows.
  </Card>
</CardGroup>

<Info>
  Loops also supports sending Supabase auth emails with [SMTP](/docs/smtp/supabase).
</Info>


# Supabase - Sync contacts
Source: https://loops.so/docs/integrations/supabase/contact-sync

Sync contacts and send emails triggered by events in Supabase.

<Tip>
  Looking to send Supabase authentication emails with Loops instead? See our [Send Email guide](/docs/integrations/supabase/send-email) (recommended) and [SMTP setup](/docs/smtp/supabase) (quicker to set up).
</Tip>

<Info>
  Our Supabase integration lets you:

  * Create and update contacts
  * Send events to trigger workflows
</Info>

Our Supabase integration is built on top of our [Incoming webhooks](/docs/integrations/incoming-webhooks) feature. This system lets you send webhooks from supported platforms directly to Loops so you can easily sync users and customers as well as send automated emails.

With Supabase, you can keep your user data synced to Loops so you can send emails to your userbase.

## Supported events

We accept the following database events for the `auth.users` table:

* `INSERT`
* `UPDATE`
* `DELETE`

[Supabase database webhooks docs](https://supabase.com/docs/guides/database/webhooks)

Events from other tables will be ignored.

## Synced data

For `INSERT` and `UPDATE` events, we sync the following Supabase data to your Loops contacts:

* User ID
* Email address
* First and last name (optional)

We use the IDs of Supabase users to match contacts in your Loops audience. If the user ID is not found in Loops, we will create a new contact.

First and last names are populated from metadata in the `auth.users` table. Key names should be `first_name` and `last_name`.

```javascript theme={"dark"}
const { data, error } = await supabase.auth.signUp({
  email: "test@example.com",
  password: "example-password",
  options: {
    data: {
      first_name: "John",
      last_name: "Doe",
    },
  },
});
```

More information about metadata in Supabase can be found in the [Supabase documentation](https://supabase.com/docs/guides/auth/managing-user-data?queryGroups=language\&language=js#adding-and-retrieving-user-metadata).

`DELETE` events can be used to delete or unsubscribe your Supabase users from your Loops audience.

## Create a webhook endpoint in Loops

[Follow the instructions here](/docs/integrations/incoming-webhooks#set-up-incoming-webhooks) to create a new webhook endpoint, which will allow you to send webhook events directly to Loops.

<img alt="Endpoint form" />

## Create a database hook in Supabase

Next, you need to set up database hooks in Supabase.

Inside a project in Supabase, go to [Integrations -> Database Webhooks -> Webhooks](https://supabase.com/dashboard/project/_/integrations/webhooks/webhooks) and click **Create a new hook**.

Give a name to your webhook, select "auth users" from the **Table** dropdown, and select the event(s) you want to get webhooks for.

Make sure **HTTP Request** is selected in the **Webhook configuration** section. In the **HTTP Request** section, paste in the endpoint URL from Loops into the **URL** field.

<img alt="Adding a webhook in Supabase" />

The last step is to secure requests with a header. In the **HTTP Headers** section, click **+ Add a new header**. From the **Secret header** section in Loops, paste in the **Header name** ("Loops-Secret") and **Header value** values into the form.

<img alt="Add header in Supabase" />

Click **Create webhook** to finish.

## Testing Supabase webhooks

You can test a user webhooks by creating, editing and deleting users from the [Authentication -> Users](https://supabase.com/dashboard/project/_/auth/users) page in Supabase, or by signing up in your application.

On Loops' end, you will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page.


# Supabase - Send emails
Source: https://loops.so/docs/integrations/supabase/send-email

Send every Supabase Auth email through Loops using the Send Email Hook, with one Loops template per auth email type.

<Tip>
  This guide only covers auth emails. To sync Supabase users into your Loops
  audience and trigger workflows, see our [Supabase contact sync guide](/docs/integrations/supabase/contact-sync).
</Tip>

Supabase Auth's [Send Email Hook](https://supabase.com/docs/guides/auth/auth-hooks/send-email-hook) lets you send emails via an HTTP endpoint you control rather than using Supabase's own sender. This guide shows how to point that hook at Loops, with a Supabase Edge Function that verifies the request and sends the matching Loops transactional email.

<Info>
  This is the recommended way to send Supabase authentication emails with
  Loops. Looking for something simpler to set up? See our [Supabase SMTP
  guide](/docs/smtp/supabase).
</Info>

## How it works

<Steps>
  <Step title="A user triggers an auth email">
    For example, they sign up, request a magic link, or ask to reset their
    password.
  </Step>

  <Step title="Supabase calls your Edge Function">
    Instead of sending the email itself, Supabase POSTs a signed payload
    containing the user and an `email_action_type` (like `signup` or
    `recovery`) to your Send Email Hook endpoint.
  </Step>

  <Step title="Your function sends the matching Loops email">
    The function verifies the request signature, maps the `email_action_type`
    to a Loops transactional email, and sends it through the [Loops
    API](/docs/api-reference/send-transactional-email) with the token and links
    Supabase provided.
  </Step>
</Steps>

## 1. Create transactional emails in Loops

[Create a transactional email](/docs/transactional) in Loops for each Supabase auth email type you want to send. You don't need to create all of them — only create templates for the action types you use, and any action type without a matching template will fall back to Supabase's default sender (see [step 3](#3-write-the-edge-function)).

| Loops template                               | `email_action_type`                  | Suggested data variables                                                  |
| :------------------------------------------- | :----------------------------------- | :------------------------------------------------------------------------ |
| Confirm signup                               | `signup`                             | `token`, `tokenHash`, `redirectTo`, `siteUrl`                             |
| Invite user                                  | `invite`                             | `token`, `tokenHash`, `redirectTo`, `siteUrl`                             |
| Magic link                                   | `magiclink`                          | `token`, `tokenHash`, `redirectTo`, `siteUrl`                             |
| Reset password                               | `recovery`                           | `token`, `tokenHash`, `redirectTo`, `siteUrl`                             |
| Change email address                         | `email_change`                       | `token`, `tokenHash`, `tokenNew`, `tokenHashNew`, `redirectTo`, `siteUrl` |
| Reauthentication                             | `reauthentication`                   | `token`, `tokenHash`                                                      |
| Security notification: password changed      | `password_changed_notification`      | —                                                                         |
| Security notification: email changed         | `email_changed_notification`         | `oldEmail`                                                                |
| Security notification: phone changed         | `phone_changed_notification`         | `oldPhone`                                                                |
| Security notification: identity linked       | `identity_linked_notification`       | `provider`                                                                |
| Security notification: identity unlinked     | `identity_unlinked_notification`     | `provider`                                                                |
| Security notification: MFA factor enrolled   | `mfa_factor_enrolled_notification`   | `factorType`                                                              |
| Security notification: MFA factor unenrolled | `mfa_factor_unenrolled_notification` | `factorType`                                                              |

<Tip>
  Supabase occasionally adds new `email_action_type` values. Check the [Send
  Email Hook
  docs](https://supabase.com/docs/guides/auth/auth-hooks/send-email-hook) for
  the current list before going live.
</Tip>

For `token`, add a `{token}` data variable to show a 6-digit OTP code. For a clickable link instead, build a URL data variable using `{siteUrl}`, `{tokenHash}` and `{redirectTo}`, for example:

```
{siteUrl}/auth/confirm?token_hash={tokenHash}&type=signup&next={redirectTo}
```

After publishing each email, copy its **Transactional ID** from the Review page — you'll need it in the next step.

## 2. Add environment variables

You'll need a Loops API key and the transactional IDs from the previous step. Create an API key from [API Settings](https://app.loops.so/settings?page=api).

```bash .env theme={"dark"}
LOOPS_API_KEY=replace-with-your-loops-api-key
SEND_EMAIL_HOOK_SECRET=replace-with-your-hook-secret

LOOPS_SIGNUP_TRANSACTIONAL_ID=replace-with-transactional-id
LOOPS_INVITE_TRANSACTIONAL_ID=replace-with-transactional-id
LOOPS_MAGICLINK_TRANSACTIONAL_ID=replace-with-transactional-id
LOOPS_RECOVERY_TRANSACTIONAL_ID=replace-with-transactional-id
LOOPS_EMAIL_CHANGE_TRANSACTIONAL_ID=replace-with-transactional-id
LOOPS_REAUTHENTICATION_TRANSACTIONAL_ID=replace-with-transactional-id
```

<Tip>
  Only add variables for the templates you created in step 1. You'll generate
  `SEND_EMAIL_HOOK_SECRET` from the Supabase dashboard in [step
  4](#4-enable-the-hook-in-supabase).
</Tip>

## 3. Write the Edge Function

Create a new Supabase Edge Function:

```bash theme={"dark"}
supabase functions new send-email
```

Supabase signs each request using the [Standard Webhooks](https://www.standardwebhooks.com/) spec, so verify it with the `standardwebhooks` library before sending anything. This function maps `email_action_type` to a Loops transactional ID and sends the email with the Loops SDK.

```typescript supabase/functions/send-email/index.ts theme={"dark"}
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
import { LoopsClient } from "npm:loops";

const loops = new LoopsClient(Deno.env.get("LOOPS_API_KEY") as string);
const hookSecret = (Deno.env.get("SEND_EMAIL_HOOK_SECRET") as string).replace(
  "v1,whsec_",
  ""
);

// Map each Supabase email_action_type to a Loops transactional email ID.
// Leave out any action type you haven't created a Loops template for.
const TRANSACTIONAL_IDS: Record<string, string | undefined> = {
  signup: Deno.env.get("LOOPS_SIGNUP_TRANSACTIONAL_ID"),
  invite: Deno.env.get("LOOPS_INVITE_TRANSACTIONAL_ID"),
  magiclink: Deno.env.get("LOOPS_MAGICLINK_TRANSACTIONAL_ID"),
  recovery: Deno.env.get("LOOPS_RECOVERY_TRANSACTIONAL_ID"),
  email_change: Deno.env.get("LOOPS_EMAIL_CHANGE_TRANSACTIONAL_ID"),
  reauthentication: Deno.env.get("LOOPS_REAUTHENTICATION_TRANSACTIONAL_ID"),
};

type EmailData = {
  token: string;
  token_hash: string;
  token_new: string;
  token_hash_new: string;
  redirect_to: string;
  email_action_type: string;
  site_url: string;
  old_email?: string;
};

Deno.serve(async (req) => {
  if (req.method !== "POST") {
    return new Response("not allowed", { status: 400 });
  }

  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
  const wh = new Webhook(hookSecret);

  try {
    const { user, email_data } = wh.verify(payload, headers) as {
      user: { email: string };
      email_data: EmailData;
    };

    const transactionalId = TRANSACTIONAL_IDS[email_data.email_action_type];

    // No Loops template mapped for this action type — let Supabase's
    // built-in sender handle it instead of failing the request.
    if (!transactionalId) {
      return new Response(JSON.stringify({}), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    }

    const response = await loops.sendTransactionalEmail({
      transactionalId,
      email: user.email,
      dataVariables: {
        token: email_data.token,
        tokenHash: email_data.token_hash,
        tokenNew: email_data.token_new ?? "",
        tokenHashNew: email_data.token_hash_new ?? "",
        redirectTo: email_data.redirect_to,
        siteUrl: email_data.site_url,
      },
    });

    if (!response.success) {
      throw new Error(response.message ?? "Failed to send Loops transactional email");
    }
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: {
          http_code: 500,
          message: error instanceof Error ? error.message : "Unknown error",
        },
      }),
      { status: 401, headers: { "Content-Type": "application/json" } }
    );
  }

  return new Response(JSON.stringify({}), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
});
```

<Warning>
  Only include data variables in the request that exist on that specific
  template. If a template doesn't have `tokenNew` or `tokenHashNew` added as
  data variables (all types other than `email_change`), it's fine to send
  them anyway — Loops ignores values that don't match a variable on the
  template.
</Warning>

Deploy the function and push your secrets:

```bash theme={"dark"}
supabase secrets set --env-file .env
supabase functions deploy send-email --no-verify-jwt
```

## 4. Enable the hook in Supabase

In your Supabase project, go to **Authentication -> Hooks** and add a new **Send Email** hook.

Select **HTTPS** as the hook type and paste in your deployed Edge Function URL (find it in **Edge Functions** after deploying).

Supabase will generate a signing secret in the form `v1,whsec_...` — copy it into `SEND_EMAIL_HOOK_SECRET` and re-run `supabase secrets set` so the deployed function has the matching value.

## Testing the hook

Create a test user from **Authentication -> Users -> Add user**, then trigger an email (for example, click **Send magic link** on that user).

Check the [Loops Transactional page](https://app.loops.so/transactional) for the send, and Supabase's **Logs -> Auth** logs (search for "magiclink" or "signup") if nothing arrives. A `401` in the auth logs usually means `SEND_EMAIL_HOOK_SECRET` doesn't match the secret shown on the Hooks page.

## Keep templates in sync from code

Because auth templates are just transactional emails, you can update them programmatically with the [Update an email message API](/docs/api-reference/update-email-message) instead of editing them by hand. Pass `expectedRevisionId` on every update so a stale edit doesn't silently overwrite someone else's change — the API returns `409 Conflict` if the revision has moved on since you last fetched it.

```typescript theme={"dark"}
import { LoopsClient, APIError } from "loops";

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

async function updateAuthTemplate(
  transactionalId: string,
  updates: { subject?: string; previewText?: string; lmx?: string }
) {
  const draft = await loops.ensureTransactionalDraft(transactionalId);

  try {
    await loops.updateEmailMessage(draft.draftEmailMessageId as string, {
      expectedRevisionId: draft.draftEmailMessageContentRevisionId,
      ...updates,
    });
  } catch (error) {
    if (error instanceof APIError && error.statusCode === 409) {
      // Someone else edited this draft after we fetched it — refetch the
      // current revision and retry once with it.
      const current = await loops.getEmailMessage(draft.draftEmailMessageId as string);
      await loops.updateEmailMessage(draft.draftEmailMessageId as string, {
        expectedRevisionId: current.contentRevisionId,
        ...updates,
      });
    } else {
      throw error;
    }
  }

  await loops.publishTransactionalEmail(transactionalId);
}
```

This is useful for keeping subject lines, preview text, or LMX content in version control and rolling changes out with a script or CI job instead of the editor. See more [email message API examples](/docs/api-reference/examples/transactional-emails).

## Send Email Hook vs Loops SMTP

|           | Send Email Hook (this guide)                                             | [Loops SMTP](/docs/smtp/supabase)                                      |
| :-------- | :----------------------------------------------------------------------- | :---------------------------------------------------------------- |
| Setup     | Deploy an Edge Function                                                  | A few clicks in Loops settings                                    |
| Templates | One Loops template per action type, edited or synced via the Content API | Shared payload structure pasted into each Supabase template field |
| Coverage  | All Send Email Hook action types, including security notifications       | Confirm signup, Magic link, Invite, Change email, Reset password  |
| Best for  | Teams that want full API/LMX control over every auth email               | Getting Supabase auth emails into Loops quickly                   |

Both approaches use the same [transactional emails](/docs/transactional) you create in Loops, so you can start with SMTP and move to the Send Email Hook later without rebuilding your templates.

## Bring in the rest of the lifecycle

The Send Email Hook only covers auth emails. Combine it with our [Supabase contact sync guide](/docs/integrations/supabase/contact-sync) to sync users to your Loops audience and trigger onboarding, activation, or plan-change workflows from the same `auth.users` events:

1. Use this guide's Send Email Hook for confirm signup, magic link, invite, reset password, and security notification emails.
2. Use the [Supabase contact sync](/docs/integrations/supabase/contact-sync#create-a-database-hook-in-supabase) database webhook to create or update Loops contacts on `INSERT` and `UPDATE` of `auth.users`.
3. Build a [workflow](/docs/workflows) triggered by the `INSERT` event to send a welcome or onboarding sequence once a user signs up.

<Tip>
  Setting this up with a coding agent? Give it this prompt:

  ```
  Use Supabase Auth and Loops for all user lifecycle email. Create Loops
  templates for signup, invite, magic link, and reset password, then wire up
  a Supabase Send Email Hook Edge Function to send them
  (https://loops.so/docs/integrations/supabase/send-email). Use the Supabase contact
  sync guide's database webhook to sync users to Loops and trigger a
  welcome workflow (https://loops.so/docs/integrations/supabase/contact-sync).
  ```
</Tip>

## Troubleshooting

If your emails are not sending:

* Confirm each transactional email in Loops is **Published**.
* Confirm `SEND_EMAIL_HOOK_SECRET` matches the secret shown on the Hooks page in Supabase, including the `v1,whsec_` prefix before it's stripped in code.
* Check that `email_action_type` in the incoming payload matches a key in `TRANSACTIONAL_IDS` — unmapped types are silently skipped rather than erroring.
* Check Supabase's **Logs -> Auth** for the response your function returned.
* Check that your Loops sending domain is verified.

## Read more

<CardGroup>
  <Card title="Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
    Loops API endpoint used to send transactional emails.
  </Card>

  <Card title="Update email message" icon="file-lines" href="/api-reference/update-email-message">
    Sync template content programmatically with revision-safe updates.
  </Card>

  <Card title="Supabase contact sync" href="/integrations/supabase/contact-sync" icon="arrows-turn-right">
    Sync contacts and trigger workflows from Supabase events.
  </Card>

  <Card title="Supabase SMTP" href="/smtp/supabase" icon="bolt">
    A simpler setup for sending Supabase auth emails through Loops.
  </Card>
</CardGroup>


# Webflow integration
Source: https://loops.so/docs/integrations/webflow

Enable signups from your site using a native Webflow form.

<Warning>
  This integration requires a paid Webflow plan to allow embedding custom
  scripts into your site.
</Warning>

To allow sign ups to your audience from your Webflow site, you can utilize a native Webflow form plus some drop-in JavaScript.

## Add a custom form script to your Webflow site

<Tip>
  If you do not add the custom script in the correct place, the form may not
  work properly.
</Tip>

To submit data to Loops seamlessly from your Webflow site we provide some JavaScript, which can be added to your site.

<CardGroup>
  <Card title="Get the script" icon="file" href="https://gist.github.com/askkaz/44ba29cf1898c60e3eb03903e63e2cc4">
    Use this script in your Webflow page.
  </Card>
</CardGroup>

### Where to add the script

* If you have a Loops form on every page of your site, add this code to the "Footer code" section in your Site settings ([read how in the Webflow docs](https://university.webflow.com/lesson/custom-code-in-the-head-and-body-tags#custom-code-in-site-settings)).
* If you have a Loops form on only one page, add this code to the "Before \</body> tag" section in your Page setting ([read how in the Webflow docs](https://university.webflow.com/lesson/custom-code-in-the-head-and-body-tags#before-the-\<-body>-tag)).

## Add a form to your page

Next you need to create a form in your Webflow page. Use the "Input" and "Button" elements.

When you add new fields, make sure the “Name” value in the field's settings panel matches the name of the field in Loops: `email`, `firstName`, etc. You can check the full list of your available properties from your [API Settings](https://app.loops.so/settings?page=api) page.

<Tip>
  Please make sure these contact properties already exist in your Loops account.
  You can add new contact properties in [API
  Settings](https://app.loops.so/settings?page=api), with a [CSV
  import](/docs/add-users/csv-upload) or [using the API](/docs/api-reference/intro).
</Tip>

<img alt="Webflow form field settings showing the “Name” attribute" />

### How to add hidden fields

You may want to assign a property to all contacts that submit the form (for example, `source` or `userGroup`).

For this add an "Embed" component *inside your form* on the same level as your input and button elements.

<img alt="Webflow embed element inside a form for hidden fields" />

In this Embed element add a hidden text field that passes on the custom value to Loops (make sure the `name` values match the "API Name" values in your [API Settings](https://app.loops.so/settings?page=api)).

```html theme={"dark"}
<input
  type="hidden"
  name="mailingLists"
  value="cly2xnjbn002z0mme68uog1wk, cly4xnjbn002x0mme28uog1wk"
/>

<input type="hidden" name="source" value="Webflow" />

<input type="hidden" name="userGroup" value="Some User Group" />

<!-- This also works with other default contact properties... -->
<input type="hidden" name="firstName" value="Barbara" />

<!-- ... and custom contact properties -->
<input
  type="hidden"
  name="signupPageUrl"
  value="https://yourdomain/this-page-url"
/>
```

<Warning>
  Mailing lists need to be marked **Public** in your [Lists
  settings](https://app.loops.so/settings?page=lists) in order for them to work
  in forms.
</Warning>

Here's how it looks in the Webflow editor:

<img alt="Webflow editor showing hidden fields embedded in the form" />

## Add your Loops form endpoint URL

The last step is to make sure your form submits data to Loops. You do this by adding a Loops form endpoint as the form's "Action" value.

1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account.
2. Click on the **Settings** tab.
3. Copy the URL shown in the **Form Endpoint** field.
   <img alt="Loops form endpoint URL in form settings" />
4. In Webflow, click on your Form Block, go to the Settings panel and paste the URL into the **Action** field.
   <img alt="Webflow form settings showing the Action URL" />

<Tip>
  Our form submission endpoint has rate limiting, so you will see an error in
  testing if you submit more than once per minute or submit the same email
  twice.
</Tip>


# Zapier integration
Source: https://loops.so/docs/integrations/zapier

Connect Loops to thousands of apps to manage contacts and send email.

<Info>
  Our Zapier integration lets you:

  * Create, find and update contacts
  * Send events to trigger workflows
  * Send transactional email
  * Trigger Zaps based on activity in your Loops account
</Info>

Zapier lets you connect thousands of other platforms to Loops.

We have created Zapier Actions for managing contacts, sending events and sending transactional emails and Zapier Triggers based on contact updates and email sending.

## Creating a new Zap

To create a new Zap—for example, to connect Tally and Loops—you can either type out what you want to create.

<img alt="Create a new Zap" />

Alternatively, click the **+ Create** button. Select Tally as the **Trigger** (using the "New Submission" event) and Loops as the **Action** (selecting "Add Contact" as the event). This would send new Tally submissions directly into Loops!

<img alt="A new Zap" />

### Authentication

To be able to use Loops Actions, you need to connect to your Loops account. From the **Account** tab, click **Sign in**.

<img alt="Sign in to Loops" />

Create or copy an API key from your Loops [API settings page](https://app.loops.so/settings?page=api) and paste it into the **API Key** field.

<img alt="Add API key into Zapier" />

If you want to remove or edit your Zapier connections to Loops, go to [Apps -> Loops](https://zapier.com/app/connections/loops).

<Tip>
  You can create connections to multiple Loops accounts from a single Zapier account.
</Tip>

## Triggers

There are a number of Loops triggers available in Zapier:

* Contact created
* Contact unsubscribed
* Contact deleted
* Contact subscribed to mailing list
* Contact unsubscribed from mailing list
* Campaign email sent
* Loops email sent
* Transactional email sent

<Tip>
  The "Email sent" triggers will fire for every individual email sent. If you send a campaign to 1,000 contacts, it will be triggered 1,000 times.
</Tip>

Accompanying data is sent with each trigger. You can see the data available for each trigger by looking up the corresponding [webhook events](/docs/webhooks).

<Info>
  Loops were renamed to Workflows on May 6, 2026. Zapier trigger bodies still
  use webhook `loop` names for compatibility, including `loop.email.sent`,
  `loopId`, `loopName`, and `sourceType: "loop"`.
</Info>

You can use the `contactIdentity` object to identify the contact that triggered each event.

For example, the "Contact subscribed to mailing list" trigger will fire when a contact is subscribed to a mailing list and contains the following data:

```json theme={"dark"}
{
  "eventName": "contact.mailingList.subscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "mailingList": {
    "id": "cm4ittp2k000l12j3lgrzvlxt",
    "name": "test mailing list",
    "description": null,
    "isPublic": true
  }
}
```

## Actions

### Add a contact

This action adds new contacts to your Loops audience. If the email address already exists, it will return an error.

All default [contact properties](/docs/contacts/properties) are available, plus [mailing lists](/docs/contacts/mailing-lists) and any custom contact properties added to your Loops account.

Only the **Email** field is required. The **Source** field defaults to “Zapier” but you can update this to whatever value you like.

<img alt="Add a contact" />

### Update a contact

<Tip>
  The "Update a contact" action will create a contact if a matching contact does not already exist, making it useful if you don't know in advance if a contact exists in Loops.
</Tip>

This action supports the same fields as "Add a contact".

Only the **Email** field is required.

<img alt="Update a contact" />

### Find a contact

This action supports searching your contact list for a specific email address.

You also have the option to create a new user if one is not found.

<img alt="Find a contact" />

### Delete a contact

This action will delete a contact by email address.

<img alt="Delete a contact" />

### Send an event

This event will send an event in Loops. You need to specify an **Email** or **User ID** value to identify the contact, plus an **Event Name**.

The action also supports [event properties](/docs/events/properties) and [mailing lists](/docs/contacts/mailing-lists).

The **Email**/**User ID** and **Event Name** fields are required.

<img alt="Send an event" />

### Send a transactional email

This action sends a transactional email and can optionally add a contact to your audience.

The **Transactional Id**, **Email** and **Data Variables** fields are required.

<img alt="Send a transactional email" />


# MCP
Source: https://loops.so/docs/mcp-server

Connect the Loops MCP server to Claude Code and Claude Desktop.

The Loops [MCP](https://modelcontextprotocol.io) server lets Claude work with your Loops account — managing contacts, campaigns, transactional emails, and more.

## Claude Code CLI

<Steps>
  <Step title="Add the Loops MCP server">
    ```bash theme={"dark"}
    claude mcp add loops https://mcp.loops.so --scope user --transport http
    ```
  </Step>

  <Step title="Authenticate">
    Run `/mcp` inside Claude Code and follow the prompt to sign in to Loops in your browser.
  </Step>

  <Step title="Test the connection">
    Ask Claude something like *"Which Loops teams am I a part of?"* to confirm it can reach your account.
  </Step>
</Steps>

## Claude Desktop

<Steps>
  <Step title="Open connector settings">
    In Claude Desktop, go to **Settings → Connectors** and click **Browse connectors**.
  </Step>

  <Step title="Add the Loops MCP server">
    Search for "Loops" and click the `+` button to the right. Click the **Connect** or **Add to your team** button.
  </Step>

  <Step title="Authenticate">
    Open the connector and follow the prompt to sign in to Loops in your browser.
  </Step>

  <Step title="Test the connection">
    Ask Claude something like *"Which Loops teams am I a part of?"* to confirm it can reach your account.
  </Step>
</Steps>


# Quickstart
Source: https://loops.so/docs/quickstart

Set up your sending domain in Loops, import contacts, build your first email, and trigger messages with events, the API, or integrations.

This is your guide to getting started with Loops. If you're new to setting up email for your SaaS company, this is the guide you should start with.

<Tip>
  Want your coding agent to handle setup? Install the Loops CLI and skills with
  `curl -fsSL https://install.loops.so/wizard | sh`, then follow the
  [Agent quickstart](/docs/quickstart-agents).
</Tip>

## What is Loops?

Loops is an email platform that helps you send marketing and transactional emails from our app, API and integrations.

With Loops, you can track events and contact properties and then use that information to send emails to increase revenue, engagement or just generally improve your user's experience of your app.

**Let's get started ✨**

What we'll be covering...

1. [Set up your domain records](#1-set-up-your-domain-records)
2. [Import contacts](#2-import-contacts)
3. [Collect signups with a form](#3-collect-signups-with-a-form)
4. [Create your first email](#4-create-your-first-email)
5. [Set up an automated mail sequence](#5-set-up-an-automated-mail-sequence)
6. [Send transactional email](#6-send-transactional-email)

## 1. Set up your domain records

The first step is to set up your domain, so you can send emails through Loops. You need to do this before you can send any emails.

We send from your domain so your emails appear as if they are coming from you.

<img alt="Set up your sending domain in Loops" />

We choose to send from a `mail.loops.so` subdomain but you can send from your root domain if you'd prefer.

To set up your domain in Loops, you need to add some MX, TXT and CNAME records to your domain's DNS settings so that we can verify that you own the domain you want to send emails from.

Once you've set up your domain records, you'll be able to start sending emails!

You can always send the records to a developer to help you integrate them. Read how to [add a member to your team](/docs/account/team-members).

<CardGroup>
  <Card title="Setting up your domain" icon="globe" href="/sending-domain" />

  <Card title="Subdomains vs root domains" icon="sitemap" href="/deliverability/sending-from-subdomain" />
</CardGroup>

## 2. Import contacts

To send marketing and product emails to your contacts, you will need to import those contacts into Loops.

Note: this isn't required if you only plan to use transactional email, as those contacts can be emailed directly via the API.

If you have any existing contacts, i.e. from a waitlist, your early access users, a database or your audience on a different platform, you can get started by importing them via CSV.

You can import contacts via [CSV upload](/docs/add-users/csv-upload), [API](/docs/api-reference/intro) or through one of our [integrations](/docs/integrations).

<Tip>
  The most popular path is to import a CSV of existing contacts, then going
  forward automatically add contacts using our API, a form or an integration.
</Tip>

### Contacts

Contacts are unique users in your Loops audience. We use email and a unique identifier to distinguish contacts. The only required field a contact must have is an email.

<img alt="Contacts table in Loops" />

### Contact properties

Contact properties are additional pieces of information you can associate with a contact. They can include things like name, location, job title, and more.

We provide [default properties](/docs/contacts/properties#default-contact-properties) like name, user group and source, but you are free to add any number of [custom properties](/docs/contacts/properties#custom-contact-properties) to your contacts, too.

<img alt="Contact properties in Loops" />

You can use contact properties to [segment your audience](/docs/contacts/filters-segments) and send more targeted emails to specific groups.

For example, you could send a promotional email only to contacts with a certain job title or to those in a specific user group.

<CardGroup>
  <Card title="Add with CSV Upload" icon="plus" href="/add-users/csv-upload" />

  <Card title="Add via integrations" icon="arrows-turn-right" href="/add-users/integrations" />

  <Card title="Add with the API" icon="rectangle-terminal" href="/add-users/loops-api" />

  <Card title="Filters and Segments" icon="filter-list" href="/contacts/filters-segments" />
</CardGroup>

## 3. Collect signups with a form

Adding a form to your site is one (popular) way to automatically add new contacts to your Audience.

Even if you're adding contacts programmatically via API or integration, in most cases you'll also want to have an input form on your page to collect emails for newsletters or product updates.

To add a form to your site, head over to the [Forms page](https://app.loops.so/forms).

You will see a handful of customization options including the form style, placeholder text, success message, font, font color, button color, and more.

Make as many changes as you need to create a form that matches your brand.

<img alt="Simple form" />

When you have finished customizing your form, simply copy the HTML or JSX that is automatically generated and paste it into your site.

For more flexibility, you can create custom HTML forms that work with Loops. [Read our full guide about custom forms](/docs/forms/custom-form) or check out our [form-based integrations](/docs/integrations#manage-contacts).

<CardGroup>
  <Card title="Simple form" icon="input-pipe" href="/forms/simple-form" />

  <Card title="Custom form" icon="table-list" href="/forms/custom-form" />

  <Card
    title="Add a form to Framer"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
    width="24"
    height="24"
    viewBox="0 0 98 148"
  >
    <path
      d="M 93.852 94.49 C 95.387 96.036 94.292 98.667 92.113 98.667 L 51.45 98.667 C 50.097 98.667 49 99.764 49 101.117 L 49 142.057 C 49 144.243 46.353 145.335 44.812 143.783 L 0.949 99.622 C 0.341 99.01 0 98.183 0 97.32 L 0 51.783 C 0 50.43 1.097 49.333 2.45 49.333 L 49 49.333 Z"
      fill="#FF4A00"
    ></path>
    <path
      d="M 49 49.333 L 4.148 4.177 C 2.613 2.631 3.708 0 5.887 0 L 95.55 0 C 96.903 0 98 1.097 98 2.45 L 98 46.883 C 98 48.236 96.903 49.333 95.55 49.333 Z"
      fill="#FF4A00"
    ></path>
  </svg>
}
    href="/integrations/framer"
  />

  <Card
    title="Add a form to Webflow"
    icon={
  <svg
    width="24"
    height="24"
    viewBox="0 0 16 16"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M15.3334 4L11.2916 12H7.49528L9.18675 8.68446H9.11086C7.7154 10.5186 5.63335 11.726 2.66675 12V8.73034C2.66675 8.73034 4.56456 8.61685 5.68023 7.42922H2.66675V4.00006H6.05357V6.8205L6.12959 6.82018L7.51356 4.00006H10.0749V6.80261L10.151 6.80249L11.5869 4H15.3334Z"
      fill="#FF4A00"
    ></path>
  </svg>
}
    href="/integrations/webflow"
  />
</CardGroup>

## 4. Create your first email

To create your first email, first select the type of content you'll be sending. You can send email as a campaign, workflow or transactional email. You can also choose to start with a [Template](https://app.loops.so/templates) instead of starting from scratch.

<img alt="Choose an email type in Loops" />

**Campaigns** are single marketing emails sent to a group (e.g. newsletters, product updates, announcements, investor updates), **Workflows** are automated emails sent based on specific triggers or conditions (e.g. onboarding sequences) and **Transactional** emails are one-off emails sent to a single person (e.g. forgot password, two-factor authorization codes, receipts). [Read more](/docs/types-of-emails)

In this example, we will build a product update (a campaign), which could be sent to your users if you're building a SaaS. They should be sent monthly or at a faster cadence depending on shipping speed and contain a high-level overview of what you shipped over the last 30 days.

To get started, click the **Create** button on the Home screen, followed by **Campaign**.

Then we’re going to personalize by adding [dynamic content](/docs/creating-emails/personalizing-emails) and [style it](/docs/creating-emails/editor) to match our brand.

<img alt="Adding personalization" />

<img alt="Adding styling" />

<Tip>
  You can preview your email any time by hitting the paper airplane icon in the
  top right of the editor window.
</Tip>

Once you're finished with the email content, click **Next** in the top right to choose your audience.

Now we'll select the audience segment to whom we'll be sending the update.

Since we're sending a product update, we want to send it to our entire audience so we won't be adding any audience filters.

If you'd like to segment your audience, just click **Add filter**, which will open the filtering options.

<img alt="Filter campaign audience" />

Click the **Next** button top right and you'll see options to send the email immediately or to schedule it for a time in the future.

<img alt="Schedule a campaign" />

Click **Next** one last time to see a review of your email and settings (you can also see a preview of your email, too). If you're happy with how everything looks, click **Schedule send** on the last page and we're done!

The email is now scheduled to go out.

By the way, you can cancel the scheduled send at any time between the send time and now to update it, or you can just send it right away.

<CardGroup>
  <Card title="Types of emails" icon="envelopes-bulk" href="/types-of-emails" />

  <Card title="Sending your first email" icon="mailbox" href="/sending-first-email" />

  <Card title="Editing emails in our editor" icon="keyboard" href="/creating-emails/editor" />

  <Card title="Custom emails with Emailify, Email Love or MJML" icon="upload" href="/creating-emails/uploading-custom-email" />
</CardGroup>

## 5. Set up an automated mail sequence

We suggest that new Loops users warm up their new sending domain with a welcome email sequence. A slow ramp up of emails sent to highly-engaged recipients will help prepare your domain for larger campaigns later on ([read more](/docs/deliverability/sending-to-large-audience)).

You can create an onboarding or welcome sequence using what we call workflows.

A workflow looks like this:

<img alt="Example workflow in Loops" />

Go to the [Workflows page](https://app.loops.so/workflows) and click **New**.

We'll start with the "Introduce yourself" template. This will create a workflow with a "Contact added" trigger (meaning every new contact will be added to the workflow), with an already-written introduction email ready for you.

Edit the email and when you're ready to make it live, click **Start**.

You can use [branches](/docs/workflows/branching) to create more complex workflows, sending contacts down different branches depending on contact properties or even whether they've interacted with campaigns you've sent from Loops.

<img alt="Branches in a workflow" />

<CardGroup>
  <Card title="Workflows" icon="arrows-rotate" href="/workflows" />

  <Card title="Triggering workflows" icon="circle-play" href="/workflows/triggers" />

  <Card title="Branching workflows" icon="code-branch" href="/workflows/branching" />
</CardGroup>

## 6. Send transactional email

You'll likely need to send a password reset, login or other automatic email that confirms a user action.

These non-promotional emails are considered **Transactional emails** and are the 1:1 emails that are sent to a single contact via API or integration.

They're included in all paid Loops plans, and also included within the 4,000 monthly sends available in the [Free plan](/docs/account/billing).

To get started, click the **Create** button on the Home screen, followed by **Transactional**.

Next, it’s time to write and style your email.

We recommend following a similar style across all of your Transactional emails. You can do this by using [themes](/docs/creating-emails/styles#themes).

Let’s create a Password Reset email together.

Add copy and styling, and then to add dynamic content click the **Insert data variables** icon and specify a data variable name.

These data variables will be populated with real content when you send the email using the API.

<img alt="Add data variables" />

Click the **Next** button top right to view the data needed in your API call.

<img alt="View the payload" />

Hit **Publish** to finalize the email. Copy the payload details and the ID; you'll need these to send the email using the API.

Make an [API request](/docs/api-reference/send-transactional-email) to the transactional endpoint (or use an [SDK](/docs/sdks)).

```
POST https://app.loops.so/api/v1/transactional
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You will need the payload copied from before. Make sure to include values for all of the data variables you added to the email.

```json theme={"dark"}
{
  "transactionalId": "clfq6dinn000yl70fgwwyp82l",
  "email": "test@example.com",
  "dataVariables": {
    "name": "Chris",
    "passwordResetLink": "https://example.com/reset-password"
  }
}
```

<Tip>
  To test sending transactional emails you can use an API tool like
  [Postman](https://www.postman.com), [Httpie](https://httpie.io) or
  [Insomnia](https://insomnia.rest) to make API requests.
</Tip>

<CardGroup>
  <Card title="About transactional email" icon="envelope" href="/transactional" />

  <Card title="Transactional guide" icon="file-code" href="/transactional" />
</CardGroup>

## 7. Integrate with other platforms

Loops integrates with thousands of other platforms, making it easy to send email to your audience, users or customers, regardless of where they originate.

<img alt="Create an action in Make" />

Set up connections to different apps using a tool like [Zapier](/docs/integrations/zapier) or [Make](/docs/integrations/make), or create contacts using events from [Segment](/docs/integrations/segment).

Our [webhook integration for Stripe](/docs/integrations/stripe) lets you easily sync customers and send automated emails based on payment events.

## Get support from the team

Whether you’re sending your very first emails for your business or are switching over from another service, we’re always here to help!

Every page of Loops has a small `?` widget in the bottom right-hand corner. Click it to receive instant support.

💬 Do you prefer live chat? Click and chat!

💌 Do you prefer email? [Send away](mailto:chris@loops.so)

🧑‍💻 Do you prefer a video call? [Book it](https://calendly.com/chris-loops/loops-in-app-support)


# Quickstart for agents
Source: https://loops.so/docs/quickstart-agents

Install the Loops CLI and agent skills so Claude Code, Codex, Cursor, or your coding agent can wire contacts, events, and transactional email into your app.

Give this page to Claude Code, Codex, Cursor, or your own agent when you want
help wiring Loops into your app. Agents can use the Loops CLI and skills to
update your code, read the right docs, and leave credentials in environment
variables.

<Tip>
  Want to set up Loops yourself? Follow the [Human quickstart](/docs/quickstart).
</Tip>

## Install Loops for agents

<Card title="Recommended setup" icon="wand-magic-sparkles">
  Install the Loops CLI and agent skills with one command.

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Card>

***

Need only one part of the setup?

<Tabs>
  <Tab title="CLI only">
    ```bash theme={"dark"}
    curl -fsSL https://install.loops.so/cli | sh
    ```
  </Tab>

  <Tab title="Skills only">
    ```bash theme={"dark"}
    curl -fsSL https://install.loops.so/skills | sh
    ```
  </Tab>
</Tabs>

Then prompt your agent:

```
Follow https://loops.so/docs/quickstart-agents. Set up Loops in this project.
Use env vars for credentials and show me what changed when you're done.
```

Ask your agent to use the [LMX](/docs/skills#lmx) and [email best practices](/docs/skills#email-best-practices-skill) skills when you need them.

<Tip>
  Keep API keys in environment variables. Do not hardcode credentials in prompts,
  scripts, or repository files.
</Tip>

## MCP and agent clients

Claude Code and Claude Desktop can connect directly to Loops through the
[Loops MCP server](/docs/mcp-server).

For Claude Code, Codex, Cursor, or another agent with terminal access, install
the Loops CLI and skills, then ask the agent to follow this page. Keep API keys
in environment variables.

## Use cases

* Generate product update emails from recent GitHub releases, merged PRs, or changelog notes.
* Sync new app users to Loops as contacts when they sign up.
* Update contact properties when a user changes plan, role, company, or onboarding status.
* Track product events like `trial_started`, `plan_upgraded`, `invite_sent`, or `onboarding_completed`.
* Send transactional emails for password resets, magic links, invoices, receipts, or team invites.
* Draft or update LMX templates for transactional and product emails.
* Add CLI smoke tests for contact creation, event tracking, and transactional sends.
* Review your email setup with the Loops email best practices skill.

## Go deeper

<CardGroup>
  <Card title="Skills" icon="book" href="/skills" />

  <Card title="MCP" icon="plug" href="/mcp-server" />

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

  <Card title="API reference" icon="code" href="/api-reference/intro" />

  <Card title="Human quickstart" icon="user" href="/quickstart" />
</CardGroup>


# Loops SDKs
Source: https://loops.so/docs/sdks

Official and community SDKs to integrate Loops into your application.

## Official SDKs

These SDKs are produced and maintained by the Loops team.

<CardGroup>
  <Card title="Go" icon="golang" href="/sdks/go">
    The official Go SDK for Loops.
  </Card>

  <Card title="JavaScript" icon="js" href="/sdks/javascript">
    The official JavaScript/TypeScript SDK for Loops.
  </Card>

  <Card title="Nuxt" icon={<svg viewBox="0 0 61 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M33.9971 39.539H56.5247C57.2402 39.539 57.9432 39.3564 58.5628 39.0093C59.1825 38.6623 59.697 38.1631 60.0546 37.5619C60.4122 36.9608 60.6003 36.2789 60.6 35.5849C60.5997 34.8908 60.411 34.2091 60.0528 33.6083L44.9239 8.1904C44.5663 7.5894 44.0519 7.09032 43.4324 6.74332C42.8129 6.39632 42.1101 6.21363 41.3947 6.21363C40.6793 6.21363 39.9766 6.39632 39.357 6.74332C38.7375 7.09032 38.2231 7.5894 37.8655 8.1904L33.9971 14.6939L26.4338 1.97648C26.0759 1.37553 25.5613 0.876518 24.9416 0.52958C24.3219 0.182643 23.619 0 22.9035 0C22.188 0 21.4851 0.182643 20.8654 0.52958C20.2457 0.876518 19.7311 1.37553 19.3732 1.97648L0.547184 33.6083C0.189033 34.2091 0.00031171 34.8908 3.85805e-07 35.5849C-0.000310938 36.2789 0.187799 36.9608 0.54541 37.5619C0.903021 38.1631 1.41752 38.6623 2.03717 39.0093C2.65681 39.3564 3.35975 39.539 4.07528 39.539H18.2162C23.819 39.539 27.9509 37.1518 30.794 32.4945L37.6965 20.8993L41.3936 14.6939L52.4895 33.3335H37.6965L33.9971 39.539ZM17.9857 33.3272L8.11711 33.325L22.9101 8.47362L30.2912 20.8993L25.3492 29.2044C23.4611 32.2262 21.3162 33.3272 17.9857 33.3272Z" fill="#FF4A00"/></svg>} href="/sdks/nuxt">
    The official Nuxt module for Loops.
  </Card>

  <Card title="PHP" icon="php" href="/sdks/php">
    The official PHP SDK for Loops.
  </Card>

  <Card title="Ruby" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="#FF4A00"><path d="M 3.125 23.160156 L 8.183594 14.480469 C 8.230469 14.394531 8.292969 14.316406 8.363281 14.253906 L 17.441406 5.792969 C 17.539063 5.703125 17.65625 5.632813 17.78125 5.585938 L 25.578125 2.722656 L 32.152344 7.316406 L 28.878906 17.722656 L 16.597656 29.132813 L 6.582031 31.378906 Z M 7.296875 33.269531 L 11.96875 46.628906 L 15.8125 31.355469 Z M 42.484375 2.875 L 35.074219 6.992188 L 47.355469 15.910156 C 47.613281 14.761719 47.914063 13.195313 47.984375 11.863281 C 47.980469 11.847656 47.980469 11.828125 47.980469 11.808594 C 48.019531 9.910156 47.707031 7.925781 46.675781 6.210938 C 45.75 4.667969 44.359375 3.539063 42.535156 2.828125 C 42.519531 2.84375 42.507813 2.863281 42.484375 2.875 Z M 34.375 35.578125 L 29.296875 20.058594 L 18.988281 29.644531 Z M 31.144531 19.269531 L 36.1875 34.679688 C 40.132813 30.386719 44.171875 25.246094 46.476563 18.925781 Z M 45.382813 16.949219 L 33.851563 8.578125 L 31.117188 17.269531 Z M 5.761719 34.9375 L 2.335938 40.816406 C 3.507813 46.242188 7.566406 47.457031 10.242188 47.753906 Z M 4.878906 32.484375 L 2 25.644531 L 2 37.421875 Z M 39.921875 2.011719 C 39.90625 2.007813 39.890625 2 39.871094 2 L 28.039063 2 L 33.3125 5.6875 Z M 17.875 31.355469 L 13.777344 47.632813 C 22.109375 46.40625 28.359375 41.929688 33.425781 37.355469 Z M 44.546875 45.648438 C 44.3125 45.648438 44.074219 45.566406 43.882813 45.398438 L 35.617188 38.058594 C 31.984375 41.429688 27.742188 44.855469 22.519531 47.164063 L 44.5625 45.667969 C 44.691406 45.660156 44.8125 45.621094 44.925781 45.570313 C 44.804688 45.621094 44.675781 45.648438 44.546875 45.648438 Z M 37.0625 36.667969 L 45.210938 43.902344 C 45.382813 44.054688 45.472656 44.253906 45.511719 44.460938 L 47.195313 22.414063 C 44.582031 28.078125 40.742188 32.730469 37.0625 36.667969 Z"></path></svg>} href="/sdks/ruby">
    The official Ruby SDK for Loops.
  </Card>
</CardGroup>

<div>
  Don't see your favorite language or framework?
  <Link href="mailto:dan@loops.so">Request an SDK</Link>
</div>

## Unofficial SDKs

The following SDKs are community-submitted and have not been officially reviewed or endorsed by Loops. We recommend thoroughly testing and reviewing the code before integrating it into your project.

* [Elixir](https://github.com/abradburne/loops_ex) by Alan Bradburne
* [Go](https://github.com/tilebox/loops-go) by Tilebox
* [Java](https://github.com/telos-tech/loops-java-sdk) by Telos Technologies
* [Laravel](https://github.com/plutolinks/laravel-loops) by PlutoLinks
* [.NET](https://github.com/soenneker/soenneker.loops.openapiclient) by Jake Soenneker
* [PHP](https://github.com/plutolinks/loops-php) by PlutoLinks
* [Python](https://github.com/doctorgpt-corp/pyloops) by Ask Aletta
* [Ruby on Rails](https://github.com/danielfriis/loops_rails) by Daniel Friis

<div>
  <Link href="mailto:dan@loops.so">Submit an SDK</Link>
</div>

## SMTP integrations

You can also send transactional email using our [SMTP service](/docs/smtp). Check out our guides for popular frameworks.

<CardGroup>
  <Card href="/smtp/laravel" icon="laravel" title="Send with SMTP from Laravel" />

  <Card
    href="/smtp/django"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="32"
    height="24"
    viewBox="0 0 32 32"
  >
    <path
      fill="#FF4A00"
      d="M14.859 0h5.234v24.219c-2.682.51-4.656.714-6.797.714-6.385 0-9.714-2.885-9.714-8.427 0-5.333 3.531-8.797 9-8.797.849 0 1.495.068 2.276.271zm0 12.193a5.225 5.225 0 0 0-1.766-.276c-2.651 0-4.177 1.63-4.177 4.49 0 2.786 1.458 4.313 4.146 4.313.578 0 1.052-.031 1.797-.135v-8.396zm13.558-4.115v12.13c0 4.177-.302 6.188-1.219 7.917-.849 1.667-1.974 2.719-4.281 3.875l-4.859-2.313c2.307-1.089 3.432-2.036 4.146-3.5.745-1.495.984-3.229.984-7.781V8.078zM23.188.026h5.229v5.37h-5.229z"
    />
  </svg>
}
    title="Send with SMTP from Django"
  />

  <Card
    href="/smtp/rails"
    icon={
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="20"
    viewBox="0 0 200 122.179"
  >
    <g fill="#FF4A00" fill-rule="evenodd">
      <path d="M6.226 122.179h69.646s-13.312-61.028 30.767-85.74c9.61-4.695 40.2-22.233 90.275 14.968 1.587-1.328 3.086-2.391 3.086-2.391s-45.843-45.97-96.887-40.833c-25.655 2.303-57.216 25.775-75.73 56.776C8.87 95.961 6.227 122.18 6.227 122.18Z" />
      <path d="M6.226 122.179h69.646s-13.312-61.028 30.767-85.74c9.61-4.695 40.2-22.233 90.275 14.968 1.587-1.328 3.086-2.391 3.086-2.391s-45.843-45.97-96.887-40.833c-25.655 2.303-57.216 25.775-75.73 56.776C8.87 95.961 6.227 122.18 6.227 122.18Z" />
      <path d="M6.226 122.179h69.646S62.56 61.441 106.639 36.846c9.61-4.673 40.2-22.127 90.275 14.898a54.832 54.832 0 0 1 3.086-2.38S154.157 3.61 103.113 8.724C77.37 11.016 45.809 34.377 27.296 65.231 8.782 96.085 6.226 122.18 6.226 122.18Zm145.11-108.518.353-5.906c-.794-.441-2.998-1.499-8.552-3.086l-.352 5.818c2.909.97 5.73 2.028 8.551 3.174Z" />
      <path d="m143.016 32.845-.352 5.547a49.29 49.29 0 0 1 8.734 1.057l.353-5.46c-3-.616-5.911-.968-8.735-1.144ZM110.372 5.37h.883L109.49 0c-2.735 0-5.558.176-8.47.528l1.677 5.196c2.558-.265 5.117-.353 7.675-.353Zm4.235 32.405 2.03 6.076c2.558-1.233 5.117-2.29 7.675-3.082l-1.94-5.812c-3 .88-5.559 1.85-7.765 2.818ZM74.552 14.617l-3.97-6.076c-2.206 1.145-4.5 2.378-6.882 3.787l4.059 6.164c2.293-1.41 4.5-2.73 6.793-3.875ZM92.64 54.243l4.235 6.34c1.5-2.202 3.264-4.227 5.205-6.252l-3.97-5.988a48.751 48.751 0 0 0-5.47 5.9ZM79.846 82.597l7.146 5.635a70.2 70.2 0 0 1 1.853-10.302l-6.352-5.02c-1.147 3.259-1.941 6.517-2.647 9.687ZM41.202 40.77l-6.264-5.46c-2.294 2.202-4.5 4.403-6.529 6.605l6.794 5.811a89.226 89.226 0 0 1 6-6.956ZM14.558 80.131 4.41 76.433C2.735 80.219.882 84.622 0 86.999l10.146 3.699c1.147-2.994 3-7.309 4.412-10.567Zm63.964 24.832c.177 4.667.618 8.453 1.059 11.095l10.587 3.786c-.794-3.434-1.588-7.308-2.117-11.447l-9.529-3.434Z" />
    </g>
  </svg>
}
    title="Send with SMTP from Rails"
  />
</CardGroup>

## Guides

<CardGroup>
  <Card title="Using the Loops SDK in Next.js" href="/guides/nextjs" icon="js" />
</CardGroup>


# Go SDK
Source: https://loops.so/docs/sdks/go

The official Loops SDK for Go.

## Installation

Install the package with `go get`:

```bash theme={"dark"}
go get github.com/loops-so/loops-go
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You will need a Loops API key to use the SDK.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an environment variable).

## Usage

```go theme={"dark"}
package main

import (
	"log"

	loops "github.com/loops-so/loops-go"
)

func main() {
	client := loops.NewClient("YOUR_API_KEY")

	err := client.SendEvent(loops.SendEventRequest{
		Email:     "user@example.com",
		EventName: "signup",
		EventProperties: map[string]any{
			"plan": "pro",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
}
```

API errors are returned as `*loops.APIError` with `StatusCode` and `Message`. See the [SDK README on GitHub](https://github.com/loops-so/loops-go) for error handling, retries, pagination, uploads, and the full method list.

See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging).

<CardGroup>
  <Card title="pkg.go.dev reference" icon="golang" href="https://pkg.go.dev/github.com/loops-so/loops-go">
    Browse types, methods, and package documentation.
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/loops-so/loops-go">
    View the source code and release notes.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>


# JavaScript SDK
Source: https://loops.so/docs/sdks/javascript

The official Loops SDK for JavaScript, with full TypeScript support.

[![NPM downloads](https://img.shields.io/npm/dt/loops?style=social\&label=Downloads)](https://www.npmjs.com/package/loops)

## Installation

Install the package [from npm](https://www.npmjs.com/package/loops):

```bash theme={"dark"}
npm install loops
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

Minimum Node version required: 18.0.0.

You will need a Loops API key to use the SDK.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an `.env` file).

<Card
  title="Next.js guide"
  icon={
<svg
  xmlns="http://www.w3.org/2000/svg"
  preserveAspectRatio="xMidYMid"
  viewBox="0 0 256 256"
>
  <path
    fill="#FF4A00"
    d="M119.617.069c-.55.05-2.302.225-3.879.35-36.36 3.278-70.419 22.894-91.99 53.044-12.012 16.764-19.694 35.78-22.597 55.922C.125 116.415 0 118.492 0 128.025c0 9.533.125 11.61 1.151 18.64 6.957 48.065 41.165 88.449 87.56 103.411 8.309 2.678 17.067 4.504 27.027 5.605 3.879.425 20.645.425 24.524 0 17.192-1.902 31.756-6.155 46.12-13.486 2.202-1.126 2.628-1.426 2.327-1.677-.2-.15-9.584-12.735-20.845-27.948l-20.47-27.648-25.65-37.956c-14.114-20.868-25.725-37.932-25.825-37.932-.1-.025-.2 16.84-.25 37.431-.076 36.055-.1 37.506-.551 38.357-.65 1.226-1.151 1.727-2.202 2.277-.801.4-1.502.475-5.28.475h-4.33l-1.15-.725a4.679 4.679 0 0 1-1.677-1.827l-.526-1.126.05-50.166.075-50.192.776-.976c.4-.525 1.251-1.2 1.852-1.526 1.026-.5 1.426-.55 5.755-.55 5.105 0 5.956.2 7.282 1.651.376.4 14.264 21.318 30.88 46.514 16.617 25.195 39.34 59.599 50.5 76.488l20.27 30.7 1.026-.675c9.084-5.905 18.693-14.312 26.3-23.07 16.191-18.59 26.626-41.258 30.13-65.428 1.026-7.031 1.151-9.108 1.151-18.64 0-9.534-.125-11.61-1.151-18.641-6.957-48.065-41.165-88.449-87.56-103.411-8.184-2.652-16.892-4.479-26.652-5.58-2.402-.25-18.943-.525-21.02-.325Zm52.401 77.414c1.201.6 2.177 1.752 2.527 2.953.2.65.25 14.562.2 45.913l-.074 44.987-7.933-12.16-7.958-12.16v-32.702c0-21.143.1-33.028.25-33.603.4-1.401 1.277-2.502 2.478-3.153 1.026-.525 1.401-.575 5.33-.575 3.704 0 4.354.05 5.18.5Z"
  />
</svg>
}
  href="/sdks/javascript/nextjs"
>
  Read our guide for sending email from Next.js projects.
</Card>

## Usage

```javascript theme={"dark"}
import { LoopsClient, APIError } from "loops";

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

try {
  const resp = await loops.createContact("test@example.com");
  // resp.success and resp.id available when successful
} catch (error) {
  if (error instanceof APIError) {
    console.log(error.json);
    console.log(error.statusCode);
  }
}
```

Import `RateLimitExceededError` to handle rate limits. See the [SDK docs](https://www.npmjs.com/package/loops) for every method, TypeScript types, rate limiting, and error handling.

See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging).

<CardGroup>
  <Card title="npm package" icon="js" href="https://www.npmjs.com/package/loops">
    View the full documentation on npm.
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/loops-so/loops-js">
    View the source code and release notes.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>


# Nuxt module
Source: https://loops.so/docs/sdks/nuxt

The official Loops Nuxt module.

[![NPM downloads](https://img.shields.io/npm/dw/nuxt-loops?style=social\&label=Downloads)](https://www.npmjs.com/package/nuxt-loops)

This Nuxt module makes it easy to add the Loops [JavaScript SDK](/docs/sdks/javascript) to your Nuxt project.

## Installation

You can install the package [from npm](https://www.npmjs.com/package/nuxt-loops):

```bash theme={"dark"}
npm install nuxt-loops
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You will need a Loops API key to use the module.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an `.env` file).

Then add `nuxt-loops` to your modules list and add a reference to your API key:

```js nuxt.config.ts theme={"dark"}
export default defineNuxtConfig({
  modules: ["nuxt-loops"],
  loops: {
    apiKey: process.env.LOOPS_API_KEY,
  },
});
```

## Usage

<Warning>
  The Loops API and SDK should only be used on the server side to protect your
  API key.
</Warning>

To use the module, import `loops` from the request context.

Then call one of the SDK methods. Read through the [JS SDK docs](https://www.npmjs.com/package/loops) for more details.

```javascript theme={"dark"}
export default defineEventHandler(async (event) => {
  const { loops } = event.context;

  const response = await loops.updateContact("test@example.com", {
    firstName: "Bri",
    lastName: "Chambers",
  });
});
```

See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging).

<CardGroup>
  <Card title="JavaScript SDK" href="/sdks/javascript" icon="js">
    Explore our official JS/TS SDK.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>


# PHP SDK
Source: https://loops.so/docs/sdks/php

The official Loops PHP package.

[![Packagist Total Downloads](https://img.shields.io/packagist/dt/loops-so/loops?style=social)](https://packagist.org/packages/loops-so/loops)

## Installation

Install the package [using Composer](https://packagist.org/packages/loops-so/loops):

```bash theme={"dark"}
composer require loops-so/loops
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You will need a Loops API key to use the SDK.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example in an environment variable).

## Usage

```php theme={"dark"}
use Loops\LoopsClient;
use Loops\Exceptions\APIError;

$loops = new LoopsClient(env('LOOPS_API_KEY'));

try {
    $result = $loops->contacts->create('test@example.com', [
        'firstName' => 'John',
    ]);
} catch (APIError $e) {
    echo $e->getMessage();
    $returnedJson = $e->getJson();
    $statusCode = $e->getStatusCode();
}
```

Use `RateLimitExceededError` to handle rate limits. See the [SDK docs](https://packagist.org/packages/loops-so/loops) for every method, namespaces, and error handling.

See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging).

<CardGroup>
  <Card title="Packagist" icon="php" href="https://packagist.org/packages/loops-so/loops">
    View the full documentation on Packagist.
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/loops-so/loops-php">
    View the source code and release notes.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>


# Ruby SDK
Source: https://loops.so/docs/sdks/ruby

The official Loops SDK for Ruby.

[![Gem Total Downloads](https://img.shields.io/gem/dt/loops_sdk?style=social)](https://rubygems.org/gems/loops_sdk)

## Installation

Add the gem to your Gemfile:

```bash theme={"dark"}
bundle add loops_sdk
```

Or install it directly:

```bash theme={"dark"}
gem install loops_sdk
```

<Tip>
  Want your coding agent to help with Loops? Install the Loops CLI and
  [skills](/docs/skills) with one command:

  ```bash theme={"dark"}
  curl -fsSL https://install.loops.so/wizard | sh
  ```
</Tip>

You will need a Loops API key to use the SDK.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example in an environment variable).

## Usage

Configure the SDK in an initializer:

```ruby config/initializers/loops.rb theme={"dark"}
require "loops_sdk"

LoopsSdk.configure do |config|
  config.api_key = ENV["LOOPS_API_KEY"]
end
```

Then call SDK methods in your application:

```ruby theme={"dark"}
begin
  response = LoopsSdk::Contacts.create(
    email: "test@example.com",
    properties: { firstName: "John" }
  )
rescue LoopsSdk::APIError => e
  puts "Loops API Error: #{e.json['message']} (Status: #{e.statusCode})"
end
```

See the [SDK docs](https://github.com/loops-so/loops-rb) for every method, rate limiting, and error handling.

See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging).

<CardGroup>
  <Card title="RubyGems" icon="gem" href="https://rubygems.org/gems/loops_sdk">
    View the gem on RubyGems.
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/loops-so/loops-rb">
    View the documentation and source code.
  </Card>

  <Card title="Loops API" icon="rectangle-terminal" href="/api-reference/intro">
    Read the Loops API reference.
  </Card>
</CardGroup>


# Security and Vulnerability Reporting
Source: https://loops.so/docs/security-reporting

How to report security or vulnerability issues.

At Loops, we care deeply about the safety and security of our customers' data and our systems. We welcome security and vulnerability reports as part of our commitment to providing the most secure product possible.

Use this page for security vulnerability reports. For security reviews,
procurement questionnaires, data residency questions, SOC 2, DPA, Data Privacy
Framework (DPF), 2FA, MFA, SSO, or other compliance requests, start with these
resources:

* [Trust center for SOC 2](https://trust.oneleet.com/loops)
* [Data Processing Agreement](https://loops.so/dpa)
* [Privacy Policy](https://loops.so/privacy) for DPF details
* [Data Privacy Framework directory](https://www.dataprivacyframework.gov/list)
  to search for Astrodon or Loops

If you need something that is not covered there, go to your
[Support page](https://app.loops.so/settings?page=support) or email
`help@loops.so`.

## Making a report

If you’ve read this document and discovered an issue that you believe is in-scope, please email us at `security@loops.so`. Please include the following details:

* A clear summary of the issue and its potential impact.
* Detailed steps to reproduce the issue.
* Relevant environmental details (browser, OS, version numbers, etc.).
* Any proof-of-concept code that demonstrates the vulnerability, if available.
* Whether or not AI was used in the generation of your report.

Our security team will review your report and keep you updated on our progress, requesting additional information or clarification when needed.

We believe that vulnerability reporting creates a safer, better product for our customers. As such, we offer compensation for reports with a CVSS score of 4 or higher.

## Timelines

We'll get back to you within a few days to acknowledge your report.

## What we’re most interested in

* Authentication bypass and privilege escalation.
* Exposure of personally identifiable information (PII).
* Unauthenticated access to user data (outside of intentionally public data).

## In scope

* [https://loops.so](https://loops.so)
* [https://app.loops.so](https://app.loops.so)
* [https://c.vialoops.com](https://c.vialoops.com)

## Out of scope

* Automated scanning.
* Social engineering.
* Denial of Service attacks.
* Attacks that need physical access to someone's computer.
* Theoretical attacks you can't actually exploit.
* Man-in-the-middle attacks.
* Clickjacking or UI redress attacks.
* CSV injection (unless it can harm non-Loops users).
* HTML injection (unless it can harm non-Loops users).
* Missing security headers, weak TLS cipher suites, or DNS setup issues. We might find these informative, but they probably won't earn a bounty.

## Please be considerate while investigating

* Only test with your own account (or get permission from the account owner first).
* Don't modify, delete, or store private data that isn't yours.
* Avoid anything that might break or slow down our services.
* If you get remote access to our systems, don't try to expand or elevate your access.

## Safe harbor

Any activities conducted in a manner consistent with this document will be considered authorized and Loops will not initiate legal action against you.


# Setting up your domain
Source: https://loops.so/docs/sending-domain

Steps for adding a sending domain to your account.

When you set up your account for the first time, you need to set up your domain records in order to start sending email. We'll be sending email on your behalf, so we need to verify that you own the domain you're sending from.

Here's how to set it up in just a few steps.

<Tip>
  If someone else manages your DNS, you can [export a zone file or share the records page](/docs/deliverability/dns-zone-file-export) instead of copying records manually.
</Tip>

<Warning>
  You need to use your own domain to send with Loops. Free subdomains provided by hosting platforms (e.g. `yourapp.vercel.app`, `yoursite.netlify.app`, `yourshop.myshopify.com`) cannot be used as sending domains, because you don't have access to configure the required DNS records.
</Warning>

## Step 1: Add your sending domain

During the sign up flow, you are asked to specify your desired sending domain. You can enter any domain here that you can set up DNS records for, and if you choose a subdomain, it doesn't need to exist yet.

We [recommend using a subdomain for this](/docs/deliverability/sending-from-subdomain), e.g. `mail.yourcompany.com`, rather than sending from your root domain `yourcompany.com`.

<img alt="Adding a domain during sign up" />

If you have already signed up and want to edit your sending domain, you can do so from **Settings -> Domain**.

<Note>
  You do not need to create this subdomain with an A or CNAME record. Loops will
  provide all DNS records you need to set up.
</Note>

## Step 2: Set up your records

From the **Settings -> Domain** page, click **View records** (or [click this link](https://app.loops.so/sending-domain) to go directly).

<img alt="Domain DNS records page in Loops" />

On this page, you'll see a few things.

**Your records**

These are SPF, DKIM and MX records that need to be set up in your domain zone editor inside of your domain registrar like Namecheap, Google Domains, AWS, GoDaddy or elsewhere.

Next to each record is a clipboard icon. You can use this to copy the records to your clipboard and easily paste them into your domain registrar.

**Your sending domain**

This is indicated below by “yourcompany.com”. This will have your domain listed. If you'd like to change domains, you can do so in the [account settings](https://app.loops.so/settings).

**A verify records button**

Once you have copied your records to your registrar, click this button to verify they have been set up correctly.

<img alt="Verify DNS records button in Loops" />

## Step 3: Add your domain records

Copy and paste the records one by one into your registrar.

You want to use the **Type** (indicated as TXT, CNAME and MX) in setting up your records, ***not*** the title of the record e.g. SPF, DKIM, MX.

<Tip>
  Loops' records for SPF are at envelope.sendingdomain.com, meaning they won't
  collide with any other SPF records you have set up. We specify a DMARC record
  so that you have one, but you can also just have a single DMARC at the root
  domain level.
</Tip>

<Note>
  Want inbox providers to display your verified logo? Set up BIMI (Brand
  Indicators for Message Identification) after your domain authentication is in
  place. Learn more in our [What is BIMI? guide](/docs/guides/what-is-bimi).
</Note>

<AccordionGroup>
  <Accordion title="Cloudflare">
    DNS records can be added from the "DNS" page within a website.

    Click **Add record** to open the form. Select a "Type" (TXT, MX or CNAME), then paste the "Name" and "Value" information.

    [Read the guide](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/#create-dns-records)

    You can also [import a zone file](/docs/deliverability/dns-zone-file-export) directly into Cloudflare instead of adding records one by one. See Cloudflare's [import and export DNS records guide](https://developers.cloudflare.com/dns/manage-dns-records/how-to/import-and-export/) for details.

    Be sure to set the proxy to “DNS Only” for the CNAME records:

    <img alt="Cloudflare DNS record form" />
  </Accordion>

  <Accordion title="Dreamhost">
    Dreamhost is currently unsupported in full because you cannot add custom MX records.
  </Accordion>

  <Accordion title="GoDaddy">
    For GoDaddy, [read this guide](https://www.godaddy.com/help/manage-dns-records-680).
  </Accordion>

  <Accordion title="Google Domains">
    Google Domains (and potentially other providers) combine the mail server and
    priority inputs into a single line. So if you receive an error like this when
    setting up the domain, make sure to instead type out the input like this: `10
                    feedback-smtp.us-east-1.amazonses.com`

    <img alt="Google Domains DNS record editor" />

    Google Domains will also include quotes “ ” around some record names. This is
    expected and will not impact anything.
  </Accordion>

  <Accordion title="Namecheap">
    Go to the **Advanced DNS** page for your domain.

    If you are using the automatic Gmail/Gsuite integration with Namecheap, you
    will need to disable the automatic integration and switch to **Custom MX** in the **Mail Settings** dropdown. You then need to [add an MX record](https://support.google.com/a/answer/174125#current\&legacy\&zippy=%2Cgoogle-workspace-current-version-later) to set up Gmail on your domain again.

    Then you can add Loops' MX records by clicking **Add new record** in the "Mail Settings" section and pasting in the values provided in Loops. Click the `✓` icon to save each record.

    <img alt="Namecheap mail settings DNS records" />

    Add the TXT and CNAME records by clicking **Add new record** in the "Host Records" section.

    <img alt="Namecheap host records DNS settings" />
  </Accordion>

  <Accordion title="Route 53">
    For Route 53, [read this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-creating.html).
  </Accordion>

  <Accordion title="Squarespace">
    For adding your records to Squarespace, [read this guide](https://support.squarespace.com/hc/en-us/articles/31120985010957-DNS-records-for-email#toc-option-2---add-custom-records-manually).

    Note that you might need to trim a trailing period from the record values.
  </Accordion>

  <Accordion title="Wix">
    Unfortunately, Wix DNS [does not support subdomains](https://support.wix.com/en/article/request-connecting-a-mailbox-to-a-subdomain) for MX records when your nameservers are pointed at Wix.

    If you purchased a domain outside of Wix, you should use the ["Pointing" method](https://support.wix.com/en/article/connecting-a-domain-to-wix-using-the-pointing-method) for your domain, which will let you set up DNS records externally at a domain registrar. Then you can add records using [this guide](https://support.wix.com/en/article/managing-dns-records-in-your-wix-account).
  </Accordion>
</AccordionGroup>

<Tip>
  Make sure you enter the “Priority” while setting up your MX record. In most
  registrars this is done by formatting it like “10 pastedrecordname”.
  Occasionally you will be asked to place it on a separate line. Just make sure
  to read the instructions on the page as you set up your MX record and if you
  have any questions, [contact our support team](https://app.loops.so/settings?page=support).
</Tip>

## Step 4: Verify your records are set up correctly

After you have copied and pasted your records into your domain registrar, click **Verify Records** at the bottom of the page to check your configuration is correct.

<Tip>
  Sometimes records can take up to an hour to propagate across all the servers.
  During that time you may see different records validate. This is totally
  normal, just check back later.
</Tip>

If the domain is set up correctly, you should see a page like the one below. If not, check back soon; sometimes records can take some time to propagate.

Notice the "Records present" in green next to each record section.

<img alt="Domain verification success in Loops" />

## Domain already in use

If you're getting a "domain already in use" error when trying to set up your domain, this typically means someone on your team has already registered an account with Loops using your domain.

Here are the steps you can take to resolve this:

<AccordionGroup>
  <Accordion title="Signing in with another account">
    Go to the [Loops login page](https://app.loops.so/) and request a login link
    using your email address.
  </Accordion>

  <Accordion title="Search your inbox">
    Search for `loopsbot@mail.loops.so` in your email inbox(es) to see if you
    have received other registration or login emails from Loops.
  </Accordion>

  <Accordion title="Check with your team">
    Ask your teammates if someone has already set up a Loops account for your
    organization. If so, ask them to invite you to the existing account.
  </Accordion>
</AccordionGroup>

If you've tried all these steps and still can't access your domain, [contact our support team](https://app.loops.so/settings?page=support) for manual assistance.

<Tip>
  Confused or have questions? Just reach out to [support](https://app.loops.so/settings?page=support).
</Tip>


# Sending your first email
Source: https://loops.so/docs/sending-first-email

A guide for creating and sending emails with Loops, including setup steps and sending best practices.

So you're ready to send your first email from Loops!

Let's go through some best practices and then see how creating an email works.

## Best practices

Here are some important things to know and bear in mind when sending email with Loops.

* We have a “low-html” editor, which means your emails send with a minimal amount of styles applied. We do this so your emails are highly readable and so they're more likely to not be placed in the spam folder or deprioritized in the inbox by your email provider.
* Try not to use sensational copy like “sale”, “discount” or exclamation points in your emails.
* It's also important to keep your emails short and to the point. Use an efficient subject line that encourages the reader to open the email and get to the point quickly in the body of the message.
* Use personalization when possible to make the message more engaging and relevant to the reader by [personalizing your emails](/docs/creating-emails/personalizing-emails).

## Send your first email

First, choose which type of email you want to send: a campaign, a workflow or a transactional email.

[Find out about the types of email](/docs/types-of-emails) you can send from Loops.

To send your first email, simply choose a template or start an email from scratch.

<img alt="Choose a template to start an email" />

## Sending settings

Along with the email subject, you can determine some of the sending settings, like the "From" sending address.

By clicking `>` you will reveal a [settings panel](/docs/creating-emails/sending-settings), where you can specify the From email address (which is always tied to your sending domain), Reply to email and the Preview text (typically shown in email clients just beneath the Subject). For workflow and transactional emails you can also specify a [CC and BCC address](/docs/creating-emails/sending-settings#cc-and-bcc).

<img alt="Email sending settings" />

You can include dynamic variables in these fields, too, making them personalized for each recipient of your campaigns, workflows and transactional emails. Just click the contact property, event property or data variable icon next to each field.

<img alt="Adding dynamic data to the sending settings" />

[Read more about sending settings](/docs/creating-emails/sending-settings)

## Add contact data

You may want to add contact data into your emails, for example, a first name or a purchased product's name. You can do this by adding dynamic content to your email.

[Learn more about personalizing emails](/docs/creating-emails/personalizing-emails)

<img alt="Insert a contact property into an email" />

## Make your message visual

When you design an email with Loops, you can add instructive screenshots, GIFs, or images to create an engaging email.

Simply drag and drop any image (including GIFs!) into the editor.

You can also easily add links, lists, buttons and dividers to create more engaging and useful emails.

We also offer a styling panel to customize design elements like font size, text color, background color, borders and spacing.

<img alt="Email editor with images and styling" />

[Read more about our editor](/docs/creating-emails/editor)

## Preview your email

Once you are happy with the design of your email, you can preview it in your email client.

Click the **Send a preview** icon in the top right corner of the email editor to send a test email to yourself or anyone on your team from the "Send a Preview Email" modal.

<img alt="Send a preview email modal" />

<img alt="Preview email modal showing contact selection" />

The **Add preview tag** toggle will add a "\[Preview]" to the start of the subject line to easily identify them in your inbox. Toggle this off to see how the email will appear in your recipients' inboxes.

### CC and BCC in previews

Preview emails don't send to any [CC or BCC addresses](/docs/creating-emails/sending-settings#cc-and-bcc) set in the email's sending settings. Previews are for checking rendering, so we skip these addresses to avoid spamming them on every test.

### Dynamic content in previews

When sending preview emails, Loops uses the contact properties and any declared fallbacks of the selected contacts in the preview modal. However, you can override this data in your preview email using the form in the modal.

### Check how your email looks on mobile

The Loops editor doesn't have a built-in mobile preview toggle. To see how your email renders on a mobile device, send a preview to yourself with **Send a preview** and open it on your phone.

Styled emails use a responsive 600px-wide layout that adapts to smaller screens automatically. If your email uses columns, you can control whether they [stack vertically on mobile](/docs/creating-emails/editor/columns#stacking-columns-on-mobile) (viewport 479px or smaller) from the **Block styles** panel.

### Test without sending emails

You can test email sending by using email addresses with `@example.com` and `@test.com` domains. This will not actually send emails but is a great way to test workflows, schedule campaigns, or test transactional emails without affecting your sending domain's reputation.

## Set up your email sending for the first time

Now you're ready to send the email!

You are able to select which contacts to send to, then either send it now or schedule it for later.

Wondering what the next step is to get started implementing emails for your SaaS? Check out the [getting started guide](/docs/quickstart).

**Did we miss something?**

Not to worry! Just [contact our support team](https://app.loops.so/settings?page=support).


# Agent skills for Loops
Source: https://loops.so/docs/skills

Install Loops agent skills in Claude Code, Codex, or Cursor so your coding agent can manage contacts, events, transactional email, and deliverability.

Skills are small modules that teach your coding agent how to work with Loops. You can install them with a single command, then your agent will be able to reference them when working with Loops.

Skills work in tools like Claude Code, Codex, and Cursor.

If your agent has terminal access, install the skills and CLI, then ask it to
use them from your project. Claude Code and Claude Desktop can also connect
through the [Loops MCP server](/docs/mcp-server).

## Installation

Install the Loops CLI and every agent skill with one command:

```bash theme={"dark"}
curl -fsSL https://install.loops.so/wizard | sh
```

Need only the skills? Use the skills installer:

```bash theme={"dark"}
curl -fsSL https://install.loops.so/skills | sh
```

The skills installer adds the Loops API, CLI, LMX, and email best practices skills.

## API skill

This skill allows your agent to build with the Loops API to manage campaigns, contacts, custom properties, mailing lists, events, and transactional emails.

Best used inside an application, like a Next.js or Nuxt.js project.

## CLI skill

This skill allows your agent to use the Loops CLI to manage contacts, mailing lists, send events, and send transactional emails.

Best used inside an LLM.

Use this when you want your agent to make real Loops calls from a terminal,
inspect IDs, or run smoke tests after adding Loops to an app.

## LMX skill

This skill allows your agent to use Loops' markup language (LMX) to create and update emails.

Best used inside an LLM.

## Email best practices skill

This skill helps your agent audit deliverability, lifecycle coverage, consent handling, and transactional vs marketing email setup.

Best used inside a project where you want help improving your email sending.

## MCP server

Claude Code and Claude Desktop can connect to Loops through the [Loops MCP
server](/docs/mcp-server). Use the MCP server to work with your Loops account from
Claude without installing the CLI.

Use agent skills with the [Loops CLI](/docs/cli) when your agent has terminal access
and needs to work in your project, edit local files, or run Loops commands.

## Links

<CardGroup>
  <Card title="Loops skills" href="https://github.com/loops-so/skills" icon="book">
    View the Loops skills repo on GitHub.
  </Card>

  <Card title="Loops CLI" href="https://github.com/loops-so/cli" icon="square-code">
    View the Loops CLI repo on GitHub.
  </Card>
</CardGroup>


# Send with SMTP
Source: https://loops.so/docs/smtp

Send Loops emails over SMTP.

You can send transactional emails over SMTP, meaning you can use Loops to power emails in platforms like Supabase and development tools and frameworks like Laravel, Rails, Django and Nodemailer.

## How it works

The Loops way of sending emails over SMTP is a bit different from other services.

<Steps>
  <Step title="Create emails in Loops">
    First, you create transactional email templates in [the Loops editor](/docs/creating-emails/editor) for the emails you want to send.

    Our rich editor helps you create beautiful, client-compatible *and* easy-to-update emails rather than hand-coding them.
  </Step>

  <Step title="Send emails from your application">
    Use these SMTP settings in your application:

    | Field       | Value                                                                                       |
    | ----------- | ------------------------------------------------------------------------------------------- |
    | Host        | `smtp.loops.so`                                                                             |
    | Port number | `587`                                                                                       |
    | Username    | `loops`                                                                                     |
    | Password    | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops |

    Then, when it comes to sending emails, instead of the content of an email, you send an API-like request body like this:

    ```json theme={"dark"}
    {
      "transactionalId": "clomzp89u035xl50px7wrl0ri",
      "email": "test@example.com",
      "dataVariables": {
        "confirmationUrl": "https://myapp.com/confirm/12345/"
      }
    }
    ```

    This content **needs to be converted to a string** and then sent as the email body.

    Loops takes the provided data and compiles an email using the template you specify in the `transactionalId` value plus the provided `dataVariables`, then sends the email to `email`.
  </Step>
</Steps>

<Warning>
  Every email sent using Loops' SMTP service requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload.
</Warning>

## SMTP Integrations

Learn how to set up SMTP in platforms and developer tools.

### Integrations

<CardGroup>
  <Card
    title="Auth0"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 285"><path fill="#FF4A00" d="M220.412 0h-92.415l28.562 89.006h92.416l-74.77 53.077 28.57 89.511c48.128-35.06 63.854-88.12 46.208-142.588L220.413 0ZM7.018 89.006h92.416L127.997 0H35.589L7.019 89.006c-17.655 54.468-1.92 107.529 46.207 142.588l28.563-89.51-74.77-53.078Zm46.208 142.588 74.77 52.97 74.77-52.97-74.77-53.847-74.77 53.847Z"/></svg>
}
    href="/integrations/auth0"
  >
    Send Auth0 authentication emails with Loops.
  </Card>

  <Card
    title="Novu"
    icon={
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" class="h-6" viewBox="0 0 102 32"><path fill="#FF4A00" fill-rule="evenodd" d="M24.64 12.826c0 .86-1.044 1.286-1.646.671L10.676.907A16 16 0 0 1 16 0c3.183 0 6.148.93 8.64 2.531zm4.48-5.986v5.986c0 4.875-5.919 7.289-9.328 3.804L6.545 3.091C2.576 6.003 0 10.701 0 16c0 3.407 1.065 6.565 2.88 9.16v-5.954c0-4.875 5.919-7.289 9.328-3.804l13.229 13.52A15.976 15.976 0 0 0 32 16c0-3.407-1.065-6.565-2.88-9.16M9.006 18.535 21.301 31.1c-1.659.583-3.443.9-5.301.9-3.182 0-6.148-.93-8.64-2.531V19.206c0-.86 1.045-1.286 1.646-.671" clip-rule="evenodd"/><path fill="#FF4A00" fill-rule="evenodd" d="M24.64 12.826c0 .86-1.044 1.286-1.646.671L10.676.907A16 16 0 0 1 16 0c3.183 0 6.148.93 8.64 2.531zm4.48-5.986v5.986c0 4.875-5.919 7.289-9.328 3.804L6.545 3.091C2.576 6.003 0 10.701 0 16c0 3.407 1.065 6.565 2.88 9.16v-5.954c0-4.875 5.919-7.289 9.328-3.804l13.229 13.52A15.976 15.976 0 0 0 32 16c0-3.407-1.065-6.565-2.88-9.16M9.006 18.535 21.301 31.1c-1.659.583-3.443.9-5.301.9-3.182 0-6.148-.93-8.64-2.531V19.206c0-.86 1.045-1.286 1.646-.671" clip-rule="evenodd"/><path fill="#FF4A00" fill-rule="evenodd" d="M24.64 12.826c0 .86-1.044 1.286-1.646.671L10.676.907A16 16 0 0 1 16 0c3.183 0 6.148.93 8.64 2.531zm4.48-5.986v5.986c0 4.875-5.919 7.289-9.328 3.804L6.545 3.091C2.576 6.003 0 10.701 0 16c0 3.407 1.065 6.565 2.88 9.16v-5.954c0-4.875 5.919-7.289 9.328-3.804l13.229 13.52A15.976 15.976 0 0 0 32 16c0-3.407-1.065-6.565-2.88-9.16M9.006 18.535 21.301 31.1c-1.659.583-3.443.9-5.301.9-3.182 0-6.148-.93-8.64-2.531V19.206c0-.86 1.045-1.286 1.646-.671" clip-rule="evenodd"/></svg>
}
    href="/integrations/novu"
  >
    Send Novu notification emails with Loops.
  </Card>

  <Card title="Supabase" icon={SupabaseIcon()} href="/smtp/supabase">
    Send Supabase authentication emails with Loops.
  </Card>
</CardGroup>

### Frameworks

<CardGroup>
  <Card title="Django" icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#FF4A00" d="M14.859 0h5.234v24.219c-2.682.51-4.656.714-6.797.714-6.385 0-9.714-2.885-9.714-8.427 0-5.333 3.531-8.797 9-8.797.849 0 1.495.068 2.276.271zm0 12.193a5.225 5.225 0 0 0-1.766-.276c-2.651 0-4.177 1.63-4.177 4.49 0 2.786 1.458 4.313 4.146 4.313.578 0 1.052-.031 1.797-.135v-8.396zm13.558-4.115v12.13c0 4.177-.302 6.188-1.219 7.917-.849 1.667-1.974 2.719-4.281 3.875l-4.859-2.313c2.307-1.089 3.432-2.036 4.146-3.5.745-1.495.984-3.229.984-7.781V8.078zM23.188.026h5.229v5.37h-5.229z"/></svg>} href="/smtp/django">
    Send transactional emails from your Django project.
  </Card>

  <Card title="Laravel" icon="laravel" href="/smtp/laravel">
    Send transactional emails from your Laravel project.
  </Card>

  <Card title="Ruby on Rails" icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 200 122.179"><g fill="#FF4A00" fill-rule="evenodd"><path d="M6.226 122.179h69.646s-13.312-61.028 30.767-85.74c9.61-4.695 40.2-22.233 90.275 14.968 1.587-1.328 3.086-2.391 3.086-2.391s-45.843-45.97-96.887-40.833c-25.655 2.303-57.216 25.775-75.73 56.776C8.87 95.961 6.227 122.18 6.227 122.18Z"/><path d="M6.226 122.179h69.646s-13.312-61.028 30.767-85.74c9.61-4.695 40.2-22.233 90.275 14.968 1.587-1.328 3.086-2.391 3.086-2.391s-45.843-45.97-96.887-40.833c-25.655 2.303-57.216 25.775-75.73 56.776C8.87 95.961 6.227 122.18 6.227 122.18Z"/><path d="M6.226 122.179h69.646S62.56 61.441 106.639 36.846c9.61-4.673 40.2-22.127 90.275 14.898a54.832 54.832 0 0 1 3.086-2.38S154.157 3.61 103.113 8.724C77.37 11.016 45.809 34.377 27.296 65.231 8.782 96.085 6.226 122.18 6.226 122.18Zm145.11-108.518.353-5.906c-.794-.441-2.998-1.499-8.552-3.086l-.352 5.818c2.909.97 5.73 2.028 8.551 3.174Z"/><path d="m143.016 32.845-.352 5.547a49.29 49.29 0 0 1 8.734 1.057l.353-5.46c-3-.616-5.911-.968-8.735-1.144ZM110.372 5.37h.883L109.49 0c-2.735 0-5.558.176-8.47.528l1.677 5.196c2.558-.265 5.117-.353 7.675-.353Zm4.235 32.405 2.03 6.076c2.558-1.233 5.117-2.29 7.675-3.082l-1.94-5.812c-3 .88-5.559 1.85-7.765 2.818ZM74.552 14.617l-3.97-6.076c-2.206 1.145-4.5 2.378-6.882 3.787l4.059 6.164c2.293-1.41 4.5-2.73 6.793-3.875ZM92.64 54.243l4.235 6.34c1.5-2.202 3.264-4.227 5.205-6.252l-3.97-5.988a48.751 48.751 0 0 0-5.47 5.9ZM79.846 82.597l7.146 5.635a70.2 70.2 0 0 1 1.853-10.302l-6.352-5.02c-1.147 3.259-1.941 6.517-2.647 9.687ZM41.202 40.77l-6.264-5.46c-2.294 2.202-4.5 4.403-6.529 6.605l6.794 5.811a89.226 89.226 0 0 1 6-6.956ZM14.558 80.131 4.41 76.433C2.735 80.219.882 84.622 0 86.999l10.146 3.699c1.147-2.994 3-7.309 4.412-10.567Zm63.964 24.832c.177 4.667.618 8.453 1.059 11.095l10.587 3.786c-.794-3.434-1.588-7.308-2.117-11.447l-9.529-3.434Z"/></g></svg>} href="/smtp/rails">
    Send transactional emails from your Rails project.
  </Card>
</CardGroup>


# Django SMTP
Source: https://loops.so/docs/smtp/django

Send transactional emails from your Django project using Loops' SMTP service.

<Tip>
  As Loops' SMTP service requires sending an API-like email body rather than a full email, it's not recommended to use Loops as the default SMTP service for your app in your settings file.\
  Instead, use a custom `connection` for each email request that you want to send through Loops.
</Tip>

Sending email from Django with Loops' SMTP service is easy but there's one gotcha: the email body needs to be an [API-like payload](/docs/smtp#how-it-works).

This may seem strange at first but it allows you to use Loops' WYSIWYG editor to craft your emails and keep email templating outside of your code repo.

We are using a custom `connection` for sending this email as typically only some emails in a project will be sent through Loops.

Add these settings to your project (e.g. in an `.env` file).

| Field       | Value                                                                                       |
| ----------- | ------------------------------------------------------------------------------------------- |
| Host        | `smtp.loops.so`                                                                             |
| Port number | `587`                                                                                       |
| Username    | `loops`                                                                                     |
| Password    | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops |

<Warning>
  Every email sent from Django over Loops SMTP requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload.
</Warning>

```python theme={"dark"}
from django.core.mail import send_mail, get_connection
import json
import os

with get_connection(
    host=os.environ['LOOPS_SMTP_HOST'],
    port=os.environ['LOOPS_SMTP_PORT'],
    username=os.environ['LOOPS_SMTP_USER'],
    password=os.environ['LOOPS_SMTP_PASSWORD'],
    use_tls=True # Has to be True
) as connection:

    email = 'test@example.com'

    # This payload can be copied from a transactional email's 
    #  Publish page in Loops
    payload = {
        "transactionalId": "clomzp89u635xl30px7wrl0ri",
        "email": email,
        "dataVariables": {
            "buttonUrl": "https://myapp.com/login/",
            "userName": "Bob"
        }
    }

    send_mail(
        "Subject here", # Overwritten by Loops template
        json.dumps(payload), # Stringify the payload
        "test@example.com", # Overwritten by Loops template
        [email],
        fail_silently=False,
        connection=connection
    )
```


# Laravel
Source: https://loops.so/docs/smtp/laravel

Send transactional emails from your Laravel project using Loops' SMTP service.

Transactional emails with Loops simplify your code. With our WYSIWYG editor and API-like payloads, you can design and manage email templates outside of your codebase, ensuring cleaner code and easier template maintenance.

<Note>
  Unlike older SMTP services, Loops requires the body of emails sent via SMTP to
  be formatted as an [API-like payload](/docs/smtp#how-it-works). This approach
  allows you to use Loops' [powerful email editor](/docs/creating-emails/editor) to
  craft your emails and keep email templating outside of your application code.
</Note>

<Warning>
  Every email sent over Loops SMTP requires a transactional email to be set up
  in your Loops account. Note the `transactionalId` value in the email payload.
</Warning>

Here's how you can set up transactional emails with Loops SMTP in Laravel:

<Steps>
  <Step title="Create emails in Loops">
    Create transactional emails in Loops using the [editor](/docs/creating-emails/editor).

    Add [data variables](/docs/transactional#add-data-variables) to your emails for any dynamic content you want to send from your Laravel application.

    <img alt="Add a data variable" />
  </Step>

  <Step title="Add SMTP variables">
    To configure Loops SMTP in your Laravel project, add the following values to your `.env` file.

    <Tip>
      `MAIL_PASSWORD` should be an API key from your [API Settings](https://app.loops.so/settings?page=api) page.
    </Tip>

    ```bash .env theme={"dark"}
    MAIL_MAILER=smtp
    MAIL_HOST=smtp.loops.so
    MAIL_PORT=587
    MAIL_USERNAME=loops
    MAIL_PASSWORD=<API_KEY>
    MAIL_ENCRYPTION=tls
    ```
  </Step>

  <Step title="Send emails from Laravel">
    Now you can send emails from your application.

    If you haven't already, create a mailable class, for example `AuthEmail`:

    ```bash theme={"dark"}
    php artisan make:mail AuthEmail
    ```

    Loops' SMTP system doesn't send full HTML emails directly. Instead, you should provide a structured API-like payload, which Loops will then use to render an HTML email.

    Create a view for your email, like below.

    You can copy an example payload from the **Publish** page of your transactional email in Loops.

    ```json resources/views/mail/auth-email-text.blade.php theme={"dark"}
    {
      "transactionalId": "clomzp89u635xl30px7wrl0ri",
      "email": "{{ $email }}", /* recipient */
      "dataVariables": {
        "loginUrl": "https://myapp.com/login?code={{ $auth_code }}"
      }
    }
    ```

    Then add a reference to your template in the `Content` definition using the `text` key.

    You also need to pass the values for the recipient email address and any data variables in your email. In this case we are using a `$user` property added to the constructor.

    ```php app/Mail/AuthEmail.php theme={"dark"}
    use App\Models\User;

    class AuthEmail extends Mailable
    {
      /**
       * Create a new message instance.
       */
      public function __construct(
          private User $user,
      ) {}

      /**
       * Get the message content definition.
       */
      public function content(): Content
      {
          return new Content(
              text: 'mail.auth-email-text',
              with: [
                'email' => $this->user->email,
                'auth_code' => $this->user->auth_code,
              ]
          );
      }
    }
    ```

    <Tip>You can omit the `view` option typically required for HTML emails in Laravel. Loops handles HTML rendering using the provided payload.</Tip>

    You can skip adding values to the `Envelope` because the "from" address and subject are all defined within Loops on your transactional email.

    Now you can send transactional emails.

    ```php theme={"dark"}
    use \App\Mail\AuthEmail;

    Mail::to('test@example.com')->send(new AuthEmail($user));
    ```

    Note that the email address defined in `to()` will not be used for sending the email even though it's a required parameter. **You have to provide the recipient's email to the template itself**.

    You can read more about sending emails from Laravel [in their docs](https://laravel.com/docs/11.x/mail#writing-mailables).
  </Step>
</Steps>

<CardGroup>
  <Card title="Send transactional email" icon="rectangle-terminal" href="/api-reference/send-transactional-email">
    Read how to send transactional email with our API.
  </Card>

  <Card title="Transactional email guide" icon="code" href="/transactional">
    Learn how to send transactional email with Loops.
  </Card>
</CardGroup>


# Ruby on Rails
Source: https://loops.so/docs/smtp/rails

Send transactional emails from your Rails project using Loops' SMTP service.

Transactional emails with Loops simplify your code. With our WYSIWYG editor and API-like payloads, you can design and manage email templates outside of your codebase, ensuring cleaner code and easier template maintenance.

<Note>
  Unlike older SMTP services, Loops requires the body of emails sent via SMTP to
  be formatted as an [API-like payload](/docs/smtp#how-it-works). This approach
  allows you to use Loops' [powerful email editor](/docs/creating-emails/editor) to
  craft your emails and keep email templating outside of your application code.
</Note>

<Warning>
  Every email sent over Loops SMTP requires a transactional email to be set up
  in your Loops account. Note the `transactionalId` value in the email payload.
</Warning>

Here's how you can set up transactional emails with Loops SMTP in Rails:

<Steps>
  <Step title="Create emails in Loops">
    Create transactional emails in Loops using the [editor](/docs/creating-emails/editor).

    Add [data variables](/docs/transactional#add-data-variables) to your emails for any dynamic content you want to send from your Rails application.

    <img alt="Add a data variable" />
  </Step>

  <Step title="Configure Action Mailer">
    To use Loops SMTP, configure Action Mailer using the following settings.

    <Tip>
      The `password` value should be an API key from your [API Settings](https://app.loops.so/settings?page=api) page.
    </Tip>

    ```ruby config/environments/production.rb theme={"dark"}
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address:         'smtp.loops.so',
      port:            587,
      user_name:       'loops',
      password:        '<API_KEY>',
      authentication:  'plain',
      enable_starttls: true
    }
    ```
  </Step>

  <Step title="Send emails from Rails">
    Now you can send emails from your application.

    Here's an example Mailer:

    ```ruby app/mailers/user_mailer.rb theme={"dark"}
    class UserMailer < ApplicationMailer

      def login_email
        @user = params[:user]
        # Note: the "to" address is required by Action Mailer
        #  but is overwritten by the email provided in the view
        #  (see below). Likewise, a subject is not required here
        #  because Loops will use the subject provided in the editor.
        mail(to: @user.email)
      end

    end
    ```

    Loops' SMTP system doesn't send full HTML emails directly. Instead, you should provide a structured API-like payload, which Loops will then use to render an HTML email.

    Create a text view for your email, like below.

    You can copy an example payload from the **Publish** page of your transactional email in Loops.

    ```json app/views/user_mailer/login_email.text.erb theme={"dark"}
    {
      "transactionalId": "clomzp89u635xl30px7wrl0ri",
      "email": "<%= @user.email %>", /* recipient */
      "dataVariables": {
        "loginUrl": "https://myapp.com/login?code=<%= @user.auth_code %>"
      }
    }
    ```

    <Tip>You do not need to provide an HTML view for your emails when using Loops SMTP (i.e.: `login_email.html.erb` is not required).</Tip>
  </Step>
</Steps>

<CardGroup>
  <Card title="Send transactional email" icon="rectangle-terminal" href="/api-reference/send-transactional-email">
    Read how to send transactional email with our API.
  </Card>

  <Card title="Transactional email guide" icon="code" href="/transactional">
    Learn how to send transactional email with Loops.
  </Card>
</CardGroup>


# Supabase SMTP
Source: https://loops.so/docs/smtp/supabase

Configure your Supabase account to send authentication emails with Loops.

<Info>
  Our [Supabase Auth Hook
  guide](/docs/integrations/supabase/send-email) is the recommended way to send Supabase authentication emails with Loops. This SMTP setup is the quicker option, covering fewer email types with a shared payload format.
</Info>

Set up an SMTP connection to send all of your Supabase emails with Loops.

There are two big benefits to using Loops for sending your Supabase emails:

<Steps>
  <Step title="More control over design">
    You can use [Loops' design editor](/docs/creating-emails/editor) to create (and
    then easily edit) beautiful transactional emails instead of having to code
    them with HTML.
  </Step>

  <Step title="Better visibility of sent emails">
    You get full visibility on which emails are being sent, when, and to whom in
    your Loops account. Supabase doesn't offer this view.
  </Step>
</Steps>

## Set up Loops SMTP in Supabase

You can set up Loops SMTP in Supabase with just a few clicks.

Go to [Settings -> Supabase](https://app.loops.so/settings?page=supabase) and click **Connect Supabase** in the **SMTP** section.

<img alt="Supabase SMTP settings" />

When prompted by Supabase, select your organization and authorize access.

Back in Loops, select your Supabase project and an API key (which is used as the SMTP password), then click **Set up SMTP**.

This will add Loops SMTP credentials to your Supabase project, so you can start sending emails. You can find these credentials in Supabase at [Authentication -> Emails -> SMTP Settings](https://supabase.com/dashboard/project/_/auth/smtp).

For reference, the SMTP credentials are:

| Field       | Value                                                                                |
| :---------- | :----------------------------------------------------------------------------------- |
| Host        | `smtp.loops.so`                                                                      |
| Port number | `587`                                                                                |
| Username    | `loops`                                                                              |
| Password    | An API key from your [API settings](https://app.loops.so/settings?page=api) in Loops |

## Create Transactional emails in Loops

Next, create new transactional emails for the emails listed in Supabase ([Authentication -> Emails](https://supabase.com/dashboard/project/_/auth/templates)). You need to create both **Confirm signup** and **Magic Link** emails to be able to properly set up the integration.

* Confirm signup (required)
* Invite user
* Magic Link (required)
* Change Email Address
* Reset Password

Note that if a Supabase user has not previously confirmed their email, they will be sent a **Confirm signup** email when you request a **Magic Link** email.

To create new transactional emails, go to the [Transactional page](https://app.loops.so/transactional) in Loops and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates).

<img alt="Supabase template in the editor" />

You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like.

You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails.

<img alt="Saved styles" />

For each Loops template you create, you need to [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Supabase to be inserted into each email.

For example, you could add a `confirmationUrl` data variable that you can map to the `{{ .ConfirmationURL }}` value from Supabase.

You can also build URLs by including values like `{{ .SiteUrl }}` or add in a confirmation code using `{{ .Token }}`.

<img alt="Supabase values" />

Once you're done creating the email and adding data variables, click **Review** in the left panel. Here you can see the API payload for your template. You will need this for the next step.

<img alt="Email payload" />

Make sure to also publish your email! It won't send unless it's published.

<CardGroup>
  <Card title="Transactional email guide" icon="code" href="/transactional">
    Read our detailed guide for sending transactional emails.
  </Card>
</CardGroup>

## Configure email templates in Supabase

The final step is to make sure your emails in Supabase are configured to send the correct data to Loops.

Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data.

In Supabase, go to [Authentication -> Emails](https://supabase.com/dashboard/project/_/auth/templates), then edit each template to contain the payload as shown in the previous step (you can click the clipboard icon in Loops to copy the full payload).

Once pasted into the Message body, you need to add the Supabase message variables into the payload.

<Warning>
  Make sure you set up at least the **Confirm signup** and **Magic Link**
  templates in Supabase, otherwise emails will not be sent. Also, any variables
  added in the **Confirm signup** template need to also be available in **Magic
  link** email, because Supabase will send a **Confirm signup** email instead of
  a **Magic Link** email if a user hasn't confirmed their email address.
</Warning>

Here is an example **Confirm signup** email template. This payload was copied from the template's Publish page in Loops, then the `{{ .Email }}` and `{{ .ConfirmationURL }}` Supabase variables were added.

```json theme={"dark"}
{
  "transactionalId": "clvmzp39u035tl50pw7wrl0ri",
  "email": "{{ .Email }}",
  "dataVariables": {
    "confirmationUrl": "{{ .ConfirmationURL }}"
  }
}
```

If you want to add each Supabase user to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ .Email }}` value.

```json theme={"dark"}
{
  "transactionalId": "clvmzp39u035tl50pw7wrl0ri",
  "email": "{{ .Email }}",
  "addToAudience": true,
  "dataVariables": {
    "confirmationUrl": "{{ .ConfirmationURL }}"
  }
}
```

Here's how the template looks in the Supabase editor:

<img alt="Supabase editor" />

With the integration now all set up, your Supabase authentication emails will be sent via Loops, giving you more visibility on your email sends and the great addition of being able to build beautiful and easy-to-update emails in the Loops editor.

<Tip>
  To view all sends of your transactional emails, click through to the email
  from the [Transactional](https://app.loops.so/transactional) page in Loops,
  where you'll find the Metrics page containing a table showing all sends and
  some statistics.
</Tip>

## Testing the integration

You can test your Loops integration by creating new users from [Authentication -> Users](https://supabase.com/dashboard/project/_/auth/users) page in Supabase.

Click **Add user** and select **Create new user**. Use an email address you have access to.

Then click on the new user in the list and click the **Send magic link** button. This will send the magic link email you set up in Supabase via Loops SMTP.

## Debugging

Supabase offers a detailed view of authentication logs where you can look for issues with your Loops integration.

Go to [Logs -> Auth](https://supabase.com/dashboard/project/_/logs/auth-logs). To narrow down your search you can look for logs like "magiclink" or "invite".

<img alt="Supabase logs" />

## Important notes

* You need to add a template in Loops and set up the email in Supabase for at least the **Confirm signup** and **Magic Link** templates.
* The subject in Supabase templates is always overwritten by the subject added to the corresponding template in Loops.
* The sender name and sender email configured in your Supabase SMTP settings are always overwritten by the sender details added to your templates in Loops.
* Any Supabase email not set up with the correct API-like payload will fail to send.


# Transactional email
Source: https://loops.so/docs/transactional

Learn how to send, test, and troubleshoot transactional email in Loops.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

Transactional emails are automated, API-triggered emails that are sent to individual contacts based on a specific action they have taken.

Examples include **confirmation emails**, **password reset emails**, and **purchase confirmations**.

<CardGroup>
  <Card title="Loops API - Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
    Read how to send transactional email with our API.
  </Card>
</CardGroup>

## How it works

Sending transactional email with Loops has two steps.

<Steps>
  <Step title="Create transactional emails">
    First, create transactional emails in Loops using our [email
    editor](/docs/creating-emails/editor). In your email you can add data variables,
    which let you insert custom data into each email you send.
  </Step>

  <Step title="Send with the API">
    To send transactional emails you need to use the Loops API. All it takes is
    a simple call to our [Send transactional email
    endpoint](/docs/api-reference/send-transactional-email). Your request needs to
    include the ID of the transactional email you created, the recipient's email
    address and any data variables needed for the email.
  </Step>
</Steps>

## Transactional vs marketing emails

Transactional emails are one-to-one messages triggered by user actions (like password resets, receipts, or login links).

Marketing emails (campaigns and workflows) are promotional or lifecycle emails sent to your Audience.

Key differences:

* Transactional emails do not include an unsubscribe link.
* Transactional emails do not track opens or link clicks.
* Unsubscribed contacts can still receive transactional emails.

You should not send marketing content as transactional email through Loops.

[Read more about the differences between transactional and marketing emails](/docs/guides/transactional-vs-marketing-email).

## Contacts

Transactional emails can be sent to any email address, whether or not the recipient exists in your Audience.

* Your Audience only contains marketing contacts. If a new contact is sent a transactional email, they are not added to your Audience.
* Sending a transactional email to a new contact will not trigger the ["Contact added" workflow trigger](/docs/workflows#triggers).
* The "Subscribed" [contact property](/docs/contacts/properties#subscribed) does not affect transactional emails. Unsubscribed contacts will still receive all transactional emails they are sent.

If you want to add a recipient to your Audience when sending a transactional email, set `"addToAudience": true` in your API request ([see below](#send-your-email)).

## Compose your email

There are two ways to create emails in Loops: using our editor or importing an MJML template.

### Use our editor

You can [create emails in the editor](/docs/creating-emails/editor), letting you easily add formatting, images and buttons.

<img alt="Example transactional email in Loops" />

You can add dynamic data into your emails by using data variables (like `name` in the example above). Read on for more information about data variables.

### Bring your own MJML

You can also [upload your own MJML code](/docs/creating-emails/uploading-custom-email) to use in the email. This is useful if you have a pre-existing template you want to use.

Like emails created in our editor, you can add data variables to MJML templates. To add a data variable called `passwordResetLink`, you can use it in your MJML like this:

```html theme={"dark"}
<mj-text>{DATA_VARIABLE:passwordResetLink}</mj-text>
```

<Tip>
  Note the uppercase “DATA\_VARIABLE” and the colon before the variable name.
</Tip>

## Add data variables

Data variables let you insert dynamic values into every transactional email, to personalize them to each recipient. When it comes to sending the email, you specify the value of each data variable in the API call.

Let's say you have a password reset email that is sent once a user clicks a "Reset Password" button in your application.

For this we need to add two data variables to the email: `name` and `resetUrl`.

You can insert data variables into the text of the email in three ways:

* click the `{}` dynamic content button above the editor (see below)
* type variables directly into the editor, like `{planName}`
* use the `/` slash menu (select "Data variable" from the menu).

<img alt="Insert data variable button in the editor" />

You can also add data variables as links (on text, images and buttons). To add the `resetUrl` as the button's link, click on the data variable icon (1), click the data variable (2) and then enter your data variable name into the **Variable name** field (3).

<img alt="Set a link to a data variable" />

If you want to write the tags manually in your content or [in MJML emails](/docs/creating-emails/uploading-custom-email), you can use our [dynamic tag syntax](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax). To add a reset URL you can write a tag like this:

```
{DATA_VARIABLE:resetUrl}
```

### Data variables in email headers

Data variables are also available in the email sending settings fields, like **From**, **Reply**, **CC**, **BCC** and **Subject**.

Click the `>` button to view all fields and then click the data variable icon. Use sensible names for your data variables, like `bccAddress` or `replyToAddress` so they are clear in your API call.

To send data to these fields, simply include the data variables in your API call as normal ([see below](#send-your-email)).

<img alt="Adding a data variable in the Subject field" />

### Optional data variables

Data variables can be optional, by selecting "Optional" from the selector just below the data variable name.

If a data variable is optional, you can omit the key from the API request entirely, or include the key and send an empty string `""` (sending `null` will not work).

<img alt="Optional data variables" />

### Array data variables

[Arrays](/docs/creating-emails/editor#arrays) are a way to add repeatable items to an email (only available in transactional emails for now).

<img alt="Previewing an array block" />

Once you add an array block to your email, you can edit its data variable name in the editor panel in the **Array** section (the default is `items`).

Any data variables you add inside the array block will be repeated for each item in the array.

Then, when [sending the email](#send-your-email), you can include an array of objects containing those variables.

<img alt="Adding data variables to an array block" />

<CardGroup>
  <Card title="Personalizing emails" icon="crystal-ball" href="/creating-emails/personalizing-emails">
    Read more about adding dynamic content into your emails.
  </Card>

  <Card title="Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
    Read how to send transactional email with the API.
  </Card>
</CardGroup>

### Important information about data variables

* Make sure that when you [send a transactional email using the API](/docs/api-reference/send-transactional-email) that you include *all non-optional data variables* in your request. If you do not include the correct set of data variables in your API call, the send will fail.
* Data variable names are case-sensitive (meaning `LastLoggedIn` and `lastLoggedIn` are different variables).
* Data variable names can only contain:
  * letters
  * numbers
  * underscores
  * dashes
* Data variable values sent over the API can be `string` or `number`.

## Review your email

On the next page, after clicking **Next**, you'll see the API Details section. This contains the data variables used in the email as well as a sample payload for reference. The “Transactional ID” lets you distinguish between different transactional emails when calling the API and is required.

<img alt="API details section before publishing" />

## Publish the email

To enable sending the email, it needs to be published by clicking **Publish**.

## Send your email

In the example above, two Data Variables were used in the email and there is a transactional ID needed as part of our API call. Any data variables created in the email are required when making the API request.

Here's an example of the request for sending this email:

**Send a POST to this endpoint (make sure to authenticate)**

```
https://app.loops.so/api/v1/transactional
```

**Payload**

You can copy an email's example payload from its Publish page in Loops by clicking the **Show payload** button.

```json theme={"dark"}
{
  "transactionalId": "clfq6dinn000yl70fgwwyp82l",
  "email": "test@example.com",
  "dataVariables": {
    "name": "Chris",
    "passwordResetLink": "https://example.com/reset-password",
    "bccAddress": "test@example.com"
  }
}
```

You can add contacts to your audience from this call by adding `"addToAudience": true` to your payload.

If you have added an [array block](/docs/transactional#array-data-variables) to your email, make sure to include the parent data variable that is tied to your array, then include an array of objects.

```json theme={"dark"}
{
  "transactionalId": "clfq6dinn000yl70fgwwyp82l",
  "email": "test@example.com",
  "dataVariables": {
    "items": [
      {
        "name": "Item 1",
        "description": "Description of Item 1"
      },
      {
        "name": "Item 2",
        "description": "Description of Item 2"
      }
    ]
  }
}
```

<CardGroup>
  <Card title="Loops API - Send transactional email" icon="envelope" href="/api-reference/send-transactional-email">
    Read how to send transactional email with our API.
  </Card>

  <Card title="JavaScript/TypeScript SDK" icon="js" href="/sdks/javascript">
    Integrate our SDK into your JS or TS application.
  </Card>
</CardGroup>

## Editing the email

To edit the email, click **Edit Draft** on the Compose page.

<img alt="Edit draft button for a transactional email" />

The previous published version of the email will remain, and will continue sending until you republish the email. This means you can make changes to transactional emails without disrupting ongoing email sending.

When you have a draft, we retain both versions and you can switch between your draft and the published version using the toggles in the top right.

<img alt="Toggle between draft and published versions" />

To publish your changes, simply click **Republish** and click the confirmation. Your draft will seamlessly become the published version.

## Metrics

After emails are sent, details are shown in the email's Metrics page.

These include send time and if messages experienced any issues with delivery (bounces or spam complaints).

<Tip>
  Note that open and click tracking is disabled for transactional messages to
  improve deliverability for infrastructure-level communications.
</Tip>

## Testing transactional emails

You can test your transactional email integration by sending to email addresses with `@example.com` and `@test.com` domains (for example `user@example.com` and `user@test.com`).

Everything will work as normal (e.g. you will receive success responses from the API), but no emails will be sent to `@example.com` or `@test.com` email addresses, making this a good way to test transactional emails without affecting your sending domain’s reputation.

## Errors

### Links look like `x-webdoc://....`

This is a known issue with Apple Mail. Make sure that your links start with `https://` or `http://` and they should work fine.

### API (400-level error)

The first place to start is to check the body of the response. It will contain a JSON object with a `message` property that will give you more information about the error. Here are common reasons that the API might return with a 400-level error:

* Using the API without a [verified domain](/docs/sending-domain).
* Trying to use the API for a transactional email without a published email message.
* Missing a required parameter: `transactionalId`, `email`, or `dataVariables`.
* Missing a data variable that is required by the email message.


# Attachments
Source: https://loops.so/docs/transactional/attachments

How to send attachments with your transactional email.

<Note>
  Need to send attachments with your transactional email? Just [reach out to
  us](https://app.loops.so/settings?page=support) and we can enable the feature on your account.
</Note>

## Sending attachments

Check out [the transactional API documentation](/docs/api-reference/send-transactional-email) for a refresher on the transactional API email payload.

To attach a file to a transactional message, you'll need to add an `attachments` key to the standard transactional API email payload. The `attachments` key should be an array of objects, each with the following keys:

* `filename` - the name of the file
* `contentType` - the MIME type of the file
* `data` - the base64 encoded content of the file

Here's an example of a transactional email payload with [this ICS file](https://gist.github.com/phil-loops/c0cb5d84d502a3949651934252d306af) attached:

```json theme={"dark"}
{
  "transactionalId": "*********",
  "email": "phil+attachments@loops.so",
  "attachments": [
    {
      "filename": "oil-change-invite.ics",
      "contentType": "text/calendar",
      "data": "QkVHSU46VkNBTEVOREFSClZFUlNJT046Mi4wClBST0RJRDotLy9pY2FsLm1hcnVkb3QuY29tLy9pQ2FsIEV2ZW50IE1ha2VyCkNBTFNDQUxFOkdSRUdPUklBTgpCRUdJTjpWVElNRVpPTkUKVFpJRDpBbWVyaWNhL0xvc19BbmdlbGVzCkxBU1QtTU9ESUZJRUQ6MjAyMzA0MDdUMDUwNzUwWgpUWlVSTDpodHRwczovL3d3dy50enVybC5vcmcvem9uZWluZm8tb3V0bG9vay9BbWVyaWNhL0xvc19BbmdlbGVzClgtTElDLUxPQ0FUSU9OOkFtZXJpY2EvTG9zX0FuZ2VsZXMKQkVHSU46REFZTElHSFQKVFpOQU1FOlBEVApUWk9GRlNFVEZST006LTA4MDAKVFpPRkZTRVRUTzotMDcwMApEVFNUQVJUOjE5NzAwMzA4VDAyMDAwMApSUlVMRTpGUkVRPVlFQVJMWTtCWU1PTlRIPTM7QllEQVk9MlNVCkVORDpEQVlMSUdIVApCRUdJTjpTVEFOREFSRApUWk5BTUU6UFNUClRaT0ZGU0VURlJPTTotMDcwMApUWk9GRlNFVFRPOi0wODAwCkRUU1RBUlQ6MTk3MDExMDFUMDIwMDAwClJSVUxFOkZSRVE9WUVBUkxZO0JZTU9OVEg9MTE7QllEQVk9MVNVCkVORDpTVEFOREFSRApFTkQ6VlRJTUVaT05FCkJFR0lOOlZFVkVOVApEVFNUQU1QOjIwMjMwOTExVDE4NTY0M1oKVUlEOjE2OTQ0NTg1ODM0MzgtOTkyMjJAaWNhbC5tYXJ1ZG90LmNvbQpEVFNUQVJUO1RaSUQ9QW1lcmljYS9Mb3NfQW5nZWxlczoyMDI1MDUwMVQxMjAwMDAKRFRFTkQ7VFpJRD1BbWVyaWNhL0xvc19BbmdlbGVzOjIwMjUwNTAxVDEyMDAwMApTVU1NQVJZOkNoYW5nZSBPaWwKVVJMOmd1bWRyb3AuZXhhbXBsZS5jb20KREVTQ1JJUFRJT046WW91ciBjYXIgaXMgcmVhZHkgZm9yIGFuIG9pbCBjaGFuZ2UhCkxPQ0FUSU9OOkd1bWRyb3AgVmlsbGFnZQpFTkQ6VkVWRU5UCkVORDpWQ0FMRU5EQVI="
    }
  ]
}
```

When you send this payload, the recipient will receive an email with the ICS file attached:

<img alt="An email with an ICS file attached" />

<Info>
  All the usual [transactional API email
  payload](/docs/api-reference/send-transactional-email) keys are still required. The
  `attachments` key is in addition to the standard payload.
</Info>

## Limitations

* The total size of the JSON request body must be less than 4 MB. Keep in mind that attachments are base64 encoded, which increases file size by \~33%.
* Attachments are not generally available. Please [contact us](https://app.loops.so/settings?page=support) if you need this feature enabled on your account.


# Types of emails
Source: https://loops.so/docs/types-of-emails

Learn about the three types of emails that you can send with Loops: Campaigns, Workflows, and Transactional.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

<img alt="Choose campaign, workflow, or transactional email" />

## Campaigns

A Campaign is the right type of email for a one-off send to your audience or a segment of your audience.

Marketing emails are a 1-to-many communication, meaning that the same exact email that you craft can (and probably will) be sent and read by a number of recipients or customers.

**Examples:**

* Newsletters
* Investor updates
* Product updates
* User feedback requests

**How to send a Campaign:**

1. Hit “Create” in the top right corner of your Loops dashboard.
2. Select “Campaign” in the popup module.
3. Enter the subject line, preview text, and content of the email.
4. Select your audience segment.
5. Schedule the email to send later or send it immediately.

## Workflows

A Workflow is an email sequence that is triggered by an event, a contact being added to your audience, or a contact property update.

**Examples:**

* Welcome emails
* User onboarding sequences
* User check-ins

For more information on sending your first workflow, [visit this guide](/docs/workflows).

## Transactional

A Transactional email is an automated message that is triggered by a specific contact action.

**Examples:**

* Password resets
* Purchase or upgrade confirmations
* Shipping information
* Account cancellation emails

For more information on sending your first Transactional messages, [read the transactional email guide](/docs/transactional).

**Things to note:**

* Unlike campaigns and workflows, transactional emails are not promotional in nature and as a result, they do not require unsubscribe information to be included in the email.
* Unlike campaigns and workflows, we do not track opens or link clicks in transactional emails, to increase deliverability of your emails. This also means that `email.opened` and `email.clicked` [webhook events](/docs/webhooks) are not available for transactional emails.
* Contacts behave slightly differently between transactional and marketing emails (campaigns and workflows):
  * Your Audience only contains marketing contacts. If a new contact is sent a transactional email, they are not added to your Audience, unless you use the `addToAudience` flag when [sending the email](/docs/api-reference/send-transactional-email).
  * Sending a transactional email to a new contact will not trigger the ["Contact added" workflow trigger](/docs/workflows#triggers).
  * The "Subscribed" [contact property](/docs/contacts/properties#subscribed) does not affect transactional emails. Unsubscribed contacts will still receive all transactional emails they are sent.

<Tip>
  Are you stuck and wondering exactly how you should be setting up your email flows within Loops?

  [Contact support](https://app.loops.so/settings?page=support) and we will help get you set up.
</Tip>


# Webhooks
Source: https://loops.so/docs/webhooks

Learn about receiving event notifications with webhooks, including setup steps and payload examples.

Webhooks send data to your website or application when certain events happen in your Loops account.

## Set up webhooks

Go to [Settings -> Webhooks](https://app.loops.so/settings?page=webhooks) and input the URL of your endpoint that will receive events.

You will be provided with a signing secret. You should save this in your project (for example in an environment variable) so you can verify requests when you receive them.

Currently you can only set up one webhook endpoint per Loops account.

Subscribe to the events you want to receive using the toggles. Click the group names to view all events in each.

<img alt="Webhooks settings" />

<Tip>
  When toggling your endpoint on and off there may be a small delay before this
  setting is reflected on the server. For example, it may take a few seconds
  after toggling on your endpoint for requests to be dispatched.
</Tip>

## Rate limiting

Webhook events will be sent at a maximum rate of 10 per second. Any further events will be queued.

## Verify requests

Every event is signed so you can check that data sent to your endpoint is sent from Loops.

To verify webhooks, you need to create a signature of the received request and match that to the provided signature in the request's headers.

Here's an example verification function you could use in Next.js:

<CodeGroup>
  ```javascript utils.ts theme={"dark"}
  import { NextRequest } from 'next/server';
  import crypto from 'crypto';

  interface WebhookVerificationError extends Error {
  code: 'MISSING_HEADERS' | 'MISSING_SECRET' | 'INVALID_SIGNATURE' | 'VERIFICATION_FAILED';
  }

  /\*\*

  - Verifies a webhook request from Loops
  - @param req The incoming Next.js request
  - @throws {WebhookVerificationError} If verification fails
  - @returns {Promise<boolean>} True if verification succeeds
    \*/
    async function verifyWebhook(req: NextRequest): Promise<boolean> {
    try {

        // Get the webhook-related headers directly from req.headers
        // Next.js automatically lowercases header names
        const eventId = req.headers.get('webhook-id');
        const timestamp = req.headers.get('webhook-timestamp');
        const webhookSignature = req.headers.get('webhook-signature');

        // Verify required headers are present
        if (!eventId || !timestamp || !webhookSignature) {
          const error = new Error('Missing required webhook header') as WebhookVerificationError;
          error.code = 'MISSING_HEADERS';
          throw error;
        }

        // Read raw body as buffer
        const readable = req.read();
        const buffer = Buffer.from(readable);
        const rawBodyText = buffer.toString();

        const signedContent = `${eventId}.${timestamp}.${rawBodyText}`;

        // Verify secret exists
        const secret = process.env.LOOPS_SIGNING_SECRET;
        if (!secret) {
          const error = new Error(
            'Missing LOOPS_SIGNING_SECRET environment variable'
          ) as WebhookVerificationError;
          error.code = 'MISSING_SECRET';
          throw error;
        }
        // Create a signature from the request data
        const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
        const signature = crypto
          .createHmac('sha256', secretBytes)
          .update(signedContent)
          .digest('base64');

        // Check if the signature matches
        const signatureFound = webhookSignature
          .split(' ')
          .some((sig) => sig.includes(`,${signature}`));

        if (!signatureFound) {
          const error = new Error('Invalid signature') as WebhookVerificationError;
          error.code = 'INVALID_SIGNATURE';
          throw error;
        }

        return true;

    } catch (error) {
    if ((error as WebhookVerificationError).code) {
    throw error;
    }
    const wrappedError = new Error(
    `Webhook verification failed: ${(error as Error).message}`
    ) as WebhookVerificationError;
    wrappedError.code = 'VERIFICATION_FAILED';
    throw wrappedError;
    }
    }

  ```

  ```javascript webhook.ts theme={"dark"}
  import { NextApiRequest, NextApiResponse } from 'next';

  export const config = {
    api: {
      bodyParser: false,
    },
  };

  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method !== 'POST') {
      return res.status(405).json({ message: 'Method not allowed' });
    }

    try {
      await verifyWebhook(req);
      // Webhook is verified, process the event...
      res.status(200).json({ message: 'Webhook processed successfully' });
    } catch (error) {
      console.error('Webhook error:', error);
      const status = {
        MISSING_HEADERS: 400,
        MISSING_SECRET: 500,
        INVALID_SIGNATURE: 401,
        VERIFICATION_FAILED: 400
      }[(error as WebhookVerificationError).code] || 500;

      return NextResponse.json(
        { message: error.message },
        { status }
      );
    }
  }
  ```
</CodeGroup>

## Testing webhooks

From the Webhooks settings page you can send a test request to your endpoint. This allows you to test that your endpoint is working, and that your verification code is OK.

The event name sent during testing is `testing.testEvent`. You can [see the payload below](#testing-testevent).

## Viewing webhook history

Once Loops has started sending webhook events to your endpoint you will be able to see event history in the **Messages** section at the bottom of the Webhooks settings page.

Clicking on an event in the table will reveal the response from your endpoint, which is helpful if there have been any errors.

We retain 30 days of event history.

<img alt="Viewing webhook event history" />

## Event data

Every webhook will contain the following data in the request body:

<Info>
  Loops were renamed to Workflows on May 6, 2026. Webhook payloads still use
  `loop` names for compatibility, including `loop.email.sent`, `loopId`,
  `loopName`, and `sourceType: "loop"`.
</Info>

### `eventName`

The event type. See a [full list of events below](#event-types).

### `webhookSchemaVersion`

Will be `1.0.0` for all events.

### `eventTime`

Unix timestamp of the time the event occurred in Loops.

***

Depending on the context of the event, more data will also be included. Full examples are shown in the [Event types](#event-types) section below.

### `contact`

A full contact object containing a contact's properties.

Contains:

* `id`
* `email`
* `firstName` (nullable string)
* `lastName` (nullable string)
* `source`
* `subscribed` (boolean)
* `userGroup`
* `userId` (nullable string)
* `mailingLists` (object with mailing list IDs as keys and `true` as the value; these are the mailing lists the contact is subscribed to)
* `optInStatus` (nullable string, `"accepted"` or `null`)
  <Note>
    This will be `null` for contacts unless they are created via a form and
    [double opt-in](/docs/contacts/double-opt-in) is enabled. `contact.created`
    events are only sent once contacts have confirmed their subscription, so
    this value will never be `"pending"` or `"rejected"`.
  </Note>
* plus any custom contact properties

This object is the same as the data returned in the [Find a contact](/docs/api-reference/find-contact#response) API endpoint.

### `contactIdentity`

A contact's identifiers. To retrieve the full contact, use the [Find a contact](/docs/api-reference/find-contact) API endpoint.

Contains:

* `id`
* `email`
* `userId` (nullable string)

### `email`

Details about an individual email send to a recipient:

* `id` - The unique ID of the email.
* `emailMessageId` - The ID of the sent version of the campaign, workflow or transactional email.
* `subject` - The subject of the sent version of the campaign, workflow or transactional email.

<Info>
  To get the ID of the campaign, workflow or transactional email that relates to the
  Loops dashboard or API, look for a `campaignId`, `loopId`, or
  `transactionalId` in the payload.
</Info>

### `mailingList`

Details about a mailing list:

* `id`
* `name`
* `description` (nullable string)
* `isPublic` (boolean)

This object is the same as the data returned in the [List mailing lists](/docs/api-reference/list-mailing-lists#response) API endpoint.

### `mailingLists`

A list of `mailingList` objects (see above), when an event relates to multiple mailing lists.

### `sourceType`

For `email.*` events, this specifies the type of email.

One of `campaign`, `loop` or `transactional`. Workflow emails use `loop`.

***

### Headers

Headers will include:

* `Webhook-Signature` - A list of request signatures, which can be used to [verify the request](#verify-requests).
* `Webhook-Id` - The unique ID of the event. You can use this to check if you have already saved or processed this specific event.
* `Webhook-Timestamp` - The timestamp of the request (seconds since epoch).

## Event types

### Contacts

#### contact.created

Sent when a new contact is created in your audience.

Contains a `contactIdentity` object plus a full `contact` object, which includes all of the new contact's properties.

<Tip>
  When [double opt-in](/docs/contacts/double-opt-in) is enabled, contact webhooks
  don't fire until the contact is confirmed. The `contact.created` event will
  only be sent for contacts created via forms once the contact has confirmed
  their subscription. Other contact-related webhooks (such as
  `contact.mailingList.subscribed`) will also only fire after confirmation.
</Tip>

```json theme={"dark"}
{
  "eventName": "contact.created",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4itta800003ow9hhekzk94o",
    "email": "test+5@loops.so",
    "userId": null
  },
  "contact": {
    "id": "cm4itta800003ow9hhekzk94o",
    "email": "test+5@loops.so",
    "firstName": null,
    "lastName": null,
    "source": "API",
    "subscribed": true,
    "userGroup": "",
    "userId": null,
    "mailingLists": {
      "cm4ittp2k000l12j3lgrzvlxt": true
    },
    "optInStatus": "accepted",
    "favoriteColor": "blue",
    "favoriteNumber": 42
  }
}
```

#### contact.unsubscribed

Sent when

* a contact is unsubscribed from your audience.
* a contact is deleted from your audience (alongside [contact.deleted](#contact-deleted)).

<Info>
  This is not the same as a contact unsubscribing from a mailing list. See
  [contact.mailingList.unsubscribed](#contact-mailingList-unsubscribed).
</Info>

Contains a `contactIdentity` object.

```json theme={"dark"}
{
  "eventName": "contact.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### contact.deleted

Sent when a contact is deleted from your audience.

Contains a `contactIdentity` object.

```json theme={"dark"}
{
  "eventName": "contact.deleted",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### contact.mailingList.subscribed

Sent when a contact is subscribed to a mailing list.

Contains `contactIdentity` and `mailingList` objects.

```json theme={"dark"}
{
  "eventName": "contact.mailingList.subscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "mailingList": {
    "id": "cm4ittp2k000l12j3lgrzvlxt",
    "name": "test mailing list",
    "description": null,
    "isPublic": true
  }
}
```

#### contact.mailingList.unsubscribed

Sent when a contact is unsubscribed from a mailing list.

<Info>
  This is not the same as a contact unsubscribing from your audience. See
  [contact.unsubscribed](#contact-unsubscribed).
</Info>

Contains `contactIdentity` and `mailingList` objects.

```json theme={"dark"}
{
  "eventName": "contact.mailingList.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "mailingList": {
    "id": "cm4ittp2k000l12j3lgrzvlxt",
    "name": "test mailing list",
    "description": null,
    "isPublic": true
  }
}
```

### Email sending

#### campaign.email.sent

Sent when a campaign is sent to a contact.

<Info>
  This event will fire for every campaign send. If you send a campaign to 1,000
  contacts, you will receive 1,000 events.
</Info>

Contains `campaignId` and `campaignName` values plus `contactIdentity` and `email` objects.

If the campaign was sent to one or more mailing lists, a `mailingLists` list will also be included.

```json theme={"dark"}
{
  "eventName": "campaign.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "campaignName": "Test Campaign",
  "email": {
    "id": "cm4t1sv84004yje79hawr1fi1",
    "emailMessageId": "cm4t1suns001ww6atotin3bn1",
    "subject": "Test Subject"
  },
  "mailingLists": [
    {
      "id": "cm4ittp2k000l12j3lgrzvlxt",
      "name": "test mailing list",
      "description": null,
      "isPublic": true
    }
  ]
}
```

#### loop.email.sent

Sent when a workflow email is sent to a contact.

<Note>
  This event keeps its pre-May 6, 2026 `loop` name for compatibility.
</Note>

<Info>
  This event will fire for every contact in a workflow. If 1,000 contacts get
  sent emails from your workflow, you will receive 1,000 events.
</Info>

Contains `loopId` and `loopName` values plus `contactIdentity` and `email` objects.

If the workflow was sent to one or more mailing lists, a `mailingLists` list will also be included.

```json theme={"dark"}
{
  "eventName": "loop.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "loopId": "cm4t1snfj0052icemfshgqfcw",
  "loopName": "Test Loop",
  "email": {
    "id": "cm4t1socj004mje79e61mgh7d",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "mailingLists": [
    {
      "id": "cm4ittp2k000l12j3lgrzvlxt",
      "name": "test mailing list",
      "description": null,
      "isPublic": true
    }
  ]
}
```

#### transactional.email.sent

Sent when a transactional email is sent.

Contains `transactionalId` and `transactionalName` values plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "transactional.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "transactionalId": "cm4t1suns001uw6atri87v54s",
  "transactionalName": "Test Transactional",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  }
}
```

### Email events

#### email.delivered

Sent when an email is delivered to its recipient.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.delivered",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.softBounced

Sent when an email soft bounces.

<Tip>
  Soft bounces are temporary email delivery failures, for example a connection
  timing out. Soft bounces are retried multiple times and sometimes the email
  is delivered.
</Tip>

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.softBounced",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.hardBounced

Sent when an email hard bounces.

<Tip>
  Hard bounces are persistent email delivery failures, for example a mailbox
  that doesn't exist. The email will not be delivered.
</Tip>

In Loops, a hard bounce results in a contact being unsubscribed from your audience so a `contact.unsubscribed` event will also be sent.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.hardBounced",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "9874cm4t1sseg004tje7982991nan8732843",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.opened

Sent when a campaign or workflow email is opened.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

<Info>
  This event is not available for transactional emails because email opens are
  [not tracked](/docs/transactional#tracking) for transactional emails.
</Info>

```json theme={"dark"}
{
  "eventName": "email.opened",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.clicked

Sent when a link in a campaign or workflow email is clicked.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

<Info>
  This event is not available for transactional emails because link clicks are
  [not tracked](/docs/transactional#tracking) in transactional emails.
</Info>

```json theme={"dark"}
{
  "eventName": "email.clicked",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.unsubscribed

Sent when a recipient unsubscribes from marketing email or a mailing list using an email's "Unsubscribe" link.

A `contact.unsubscribed` or `contact.mailingList.unsubscribed` event will also be sent depending on whether the email was sent to a mailing list or not.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

<Info>
  This event is not available for transactional emails because unsubscribe links
  are [not included or required](/docs/types-of-emails#transactional) for
  transactional emails.
</Info>

```json theme={"dark"}
{
  "eventName": "email.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.resubscribed

Sent when a recipient resubscribes to marketing email from an email's preference center ("Unsubscribe" link).

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

<Info>
  This event is not available for transactional emails because unsubscribe links
  are [not included or required](/docs/types-of-emails#transactional) for
  transactional emails.
</Info>

```json theme={"dark"}
{
  "eventName": "email.resubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.spamReported

Sent when a recipient reports your email as spam.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.spamReported",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

### Testing

#### testing.testEvent

This is a test event that can be triggered at any time from the [Webhooks settings page](https://app.loops.so/settings?page=webhooks) in Loops.

```json theme={"dark"}
{
  "eventName": "testing.testEvent",
  "eventTime": 1734425918,
  "message": "test",
  "webhookSchemaVersion": "1.0.0"
}
```


# Workflows
Source: https://loops.so/docs/workflows

Workflows let you send emails based on something happening, like a contact property updating, a new contact being created or an external event happening in another platform.

<Tip>
  To manage Loops with an AI agent, [connect Loops to Claude](/docs/mcp-server), or
  install the [Loops CLI and agent skills](/docs/quickstart-agents) for Claude Code,
  Codex, or Cursor.
</Tip>

## Getting Started

To start building your workflow, select a template or start from scratch.

<img alt="Workflow template selection" />

Templates are added often and we're always open to taking suggestions!

## Building a workflow

A workflow is an email sequence that can be triggered by different events and contain emails, delay timers and branches.

You can add new nodes to your workflow by hovering over an arrow between nodes and clicking the `+` button.

More complex workflows can be built by [adding branches](/docs/workflows/branching).

<img alt="Workflow builder canvas with nodes" />

## Editor panels

The workflow editor has a navigation panel on the left-hand side. This navigation panel provides a dropdown of workflows to jump to, a command search, and **Build** and **Metrics** views. The inspector panel on the right-hand side shows settings for the selected node, workflow status, and the **Start** and **Pause** toggle button. When nothing is selected, the inspector panel shows validation checks to make sure the workflow meets all the requirements to start running.

You can collapse or expand either panel with its panel button. On desktop, press `⌘/Ctrl + /` to toggle both panels.

## Zoom and canvas navigation

You can zoom in and pan around the workflow canvas, which makes it easier to work with larger and multi-branch workflows.

Zoom and panning uses typical trackpad and mouse gestures, so should be familiar.

**On a trackpad**

* Pinch to zoom in and out
* Use two fingers to pan around the canvas

**With a scroll-wheel mouse**

* Use `⌘/Ctrl` and the scroll wheel to zoom
* Use the scroll wheel to pan vertically
* Use `Shift` and the scroll wheel to pan horizontally

To reset your view back to 100%, click the "Zoom" button that appears at the bottom of the canvas.

## Mailing lists

You can make a workflow send only to contacts on a specific mailing list by using the mailing list dropdown in the inspector panel.

This makes sure that only contacts from that list are added to the workflow.

<img alt="Mailing list selector in workflow builder" />

<Note>
  You can also change the selected list from the workflow summary in the
  inspector panel.
</Note>

## Nodes

There are six types of nodes available in workflows:

* Trigger
* Email
* Timer
* Audience filter
* Branch ([read more](/docs/workflows/branching))
* Experiment ([read more](/docs/workflows/experiments))

### Triggers

<img alt="Workflow trigger options" />

The first node to add to a workflow is the trigger. This is what will send new people into the workflow.

There are four trigger options:

* **Contact added**: Triggered whenever a contact has been added to your audience.
  <Info>
    Only contacts who have been added via a [form](/docs/forms/simple-form), the
    [API](/docs/api-reference/intro) or an [integration](/docs/integrations) will be added
    based on this trigger. Contacts added individually to the audience table
    will not be included. Contacts added or updated via CSV will only trigger if
    you select the [Trigger workflows](/docs/add-users/csv-upload#trigger-workflows-via-csv)
    toggle during upload.
  </Info>
* **Contact updated:** Triggered whenever a contact property changes from one value to another. Can also conditionally trigger only if the previous property matches the inputted value.
* **Contact added to list:** Triggered whenever a contact is added to a certain [mailing list](/docs/contacts/mailing-lists). You can select the list from the trigger node.
* **Event received**: Triggered when a certain [event](/docs/events) is sent to Loops by the API or an integration. With this trigger you can start a workflow based on interactions in your app. Common custom events are `signUp`, `canceled` and `activated`.

You can read more about triggers [here](/docs/workflows/triggers).

### Emails

You can add emails at any location inside your workflow, giving you flexibility to send mail immediately after the trigger or after a delay. You can also send to specific contacts by adding an audience filter, or by branching your workflows.

<img alt="Email node configuration in workflow builder" />

You can add unlimited emails to each workflow.

To edit an email, click on the email node, then click **Edit email** in the inspector panel.

When a workflow is active, you cannot edit the email. Click **Pause** in the inspector panel header on desktop (or in the top nav on mobile) to pause the workflow and make changes to your email (and any other nodes). While paused, new contacts will be queued for up to 24 hours; they will enter your workflow as soon as you save your email by clicking **Resume**.

You can copy emails from other workflows by clicking the **Copy existing email** button in the inspector panel after creating a new email node. This will bring up a dropdown of emails from other workflows that you can insert into your workflow.

<img alt="Duplicating emails" />

### Timers

<img alt="Timer node in workflow builder" />

A timer adds a fixed time period between two nodes in the workflow.

For example, you could add a "3 day" timer after a "Contact added" trigger to send an email three days after a signup.

You can select the “Immediately” option to bypass the timer or any increment of time to extend the duration of the workflow.

You can add multiple timers to your workflows, to add delays in different parts of your workflow.

### Audience filters

<img alt="Audience filter node in workflow builder" />

Audience filters let you create fine-tuned workflows to target specific contacts.

For example, you can check in with contacts that have not signed up as a paying user 3 days after signing up by setting the Custom Event to `signup` the Timer duration to 3 days and the audience filter to `paid` equals `false`.

There are two options for applying audience filters:

* **All following nodes**: this will apply the filter when contacts reach every following node. If a contact no longer matches the filter's audience when they reach a following node, they will be removed from the workflow.
* **Next node only**: this will apply your filter before contacts reach the next node. This is a one-time filter and any changes to the contact's value after the filter will not be taken into account.

<CardGroup>
  <Card title="Branching workflows" icon="code-branch" href="/workflows/branching">
    Learn how to branch workflows using audience filters.
  </Card>
</CardGroup>

## Metrics

Click **Metrics** in the navigation panel to view simple metrics inline within the builder.

<img alt="Workflow metrics tab" />

Click **View metrics** to view detailed metrics for an email in your workflow.

<img alt="Workflow metrics details view" />

## Testing workflows

You can test if a workflow works as expected by using email addresses with `@example.com` and `@test.com` domains (for example `test@example.com`).

First, add these as contacts in your audience, then depending on how your workflows are set up, you can add and update properties, or send events to these email addresses to see how contacts move through your workflows.

Emails will not be sent to `@example.com` or `@test.com` email addresses so this is a good method to test emailing contacts without affecting your sending domain’s reputation.

You can also send test emails for each of the emails in a workflow. Click on an email node and then the **Send a preview** airplane icon in the **Email preview** section of the inspector panel.


# Branching workflows
Source: https://loops.so/docs/workflows/branching

Branching allows you to send different emails based on a contact's properties within a single workflow.

A common use case for crafting a user journey is to send different emails based on whether a contact has completed an action or not.

For example, you may want to send a different email to free users versus paid users.

You can do this by adding a **Branch** node to your workflow.

<img alt="Branches in a workflow" />

## Creating a branching workflow

<img alt="Adding a node to a branch" />

To create a branching workflow, click the `+` icon where you want branching to be added and select the **Branch** node type.

Two audience filter nodes are automatically created for you. Edit the audience filter settings for each to send different contacts down each branch, based on their properties.

You can add as many branches as you like to your workflow. Add more branches by clicking the `+ Branch` option that appears on hover just below the Branch node.

Each branch can contain emails, timers, audience filters, more branches, and experiments.

<CardGroup>
  <Card title="Node types" icon="input-text" href="/workflows#nodes">
    Learn about the different node types.
  </Card>

  <Card title="Experiments" icon="vial" href="/workflows/experiments">
    Add A/B testing to your workflows.
  </Card>
</CardGroup>

## Audience filters

Every branch is defined by an audience filter, which is always the first node in a branch. Filters determine which contacts follow each branch.

When a contact reaches a branch node, the audience filter nodes are evaluated left-to-right as defined in the workflow builder. The contact will proceed to the first matching filter and the remaining filters are skipped. If the contact matches none of the filters, they exit the workflow.

You can create the equivalent of a "default path" by making the right-most filter have empty conditions so that it matches any contact.

<Note>
  #### Upgrade notice

  Previously, a branch node would send contacts who matched multiple filters down multiple parallel branches.

  This behavior has been changed; now, in any new branches added to workflows, contacts will only follow the first branch they match.

  Branches with the old behavior are marked in your workflows. They will retain the old logic until you explicitly press the "Upgrade Branch Node" button shown in the Branch head node's options panel. For more information, see the [branch node upgrade documentation](/docs/workflows/branch-node-upgrade).
</Note>

The audience filter nodes created after a branching node can be toggled between two settings:

* **All following nodes:** The audience filter will apply to *all nodes* downstream of the filter. If a contact stops matching the filter later down the branch, they will be removed from the workflow.
* **Next node only:** The audience filter will only apply when contacts reach the next node in the branch. If a contact stops matching the filter later in the branch, they will remain in the workflow.

<CardGroup>
  <Card title="Audience filters" icon="filter" href="/workflows#audience-filters">
    Read more about audience filters.
  </Card>
</CardGroup>

## Converging branches

A common use case is to have different branches converge back to the same path. For example, you may want to create branches with different filters that lead to different onboarding emails, but have all branches converge on a single timer that waits a week before sending the same follow-up email.

Here's how you can set up converging paths in a workflow:

<Steps>
  <Step title="Reroute a connection">
    Click **Reroute connection** located below a node.

    <img alt="Click reroute button" />
  </Step>

  <Step title="Click on a target node">
    Now select the target node you want to reroute to.

    Since workflows do not allow circular paths, only valid target nodes will be visible, and any invalid target nodes will be dimmed.

    <img alt="Click target node" />
  </Step>

  <Step title="End result">
    You'll end up with multiple nodes converging on the same target.

    <img alt="Rerouting done" />
  </Step>
</Steps>

## Deleting branches

To delete all branches, select the head Branch node and click the trash icon. This will remove all branches—including all nodes—following it.

<img alt="Delete branch node" />

If you want to delete a single branch, you can do so by deleting each node in the branch you want removed. When the final node is deleted, the branch will be removed.

<img alt="Delete single branch" />


# Experiments
Source: https://loops.so/docs/workflows/experiments

Learn how to use experiments to test different versions of your emails.

Experiments are a way to run split tests within workflows. This lets you test different versions of your emails, allowing you to see which version performs best.

You can test things like subject lines, preheaders, and content, and specify the percentage of contacts who should be sent down each variant branch.

## Creating an experiment

To add an experiment, add a new node by clicking the `+` button in the workflow builder and select **Experiment**. You can add an experiment to any workflow.

<img alt="Adding an experiment" />

You can add multiple experiments to a workflow, even within other experiments if you want. Each experiment can contain any number of variants.

Each variant can contain emails, timers and audience filters.

<img alt="Experiment with emails and filters" />

## Variants and controls

Experiments are made up of variant and control branches.

* **Variant** branches contain the email(s) you are testing, which will be sent to a specific percentage of contacts that you define.
* The **Control** branch contains the email(s) that will be sent to all contacts not included in the experiment, and should be used as the baseline for the experiment. A control is optional; you can create an experiment without a control branch.

<Tip>
  An experiment node must be followed by at least one variant or control branch, otherwise you cannot start the workflow.
  Likewise, you must add at least one email within each control or variant branch before starting the workflow.
</Tip>

## Sample size

You can specify the percentage of contacts who will be sent to control and variant branches by dragging the slider in Experiment nodes.

<img alt="Changing sample size" />

Every variant you add will receive an equal percentage of contacts within the experiment sample. For example, if you set the sample size to 60% and add three variants and a control, 20% of contacts will be sent to each variant branch (60% / 3) and 40% of contacts will be sent to the control branch.

You can change a variant to be the control by clicking on the variant node and toggling the **Use as control** option.

<img alt="Changing the control node" />

If you do not add a control branch, only your selected sample size will be sent through the experiment node. For example, if you have three variants and no control and select 60% for your sample size, 40% of contacts entering the workflow will exit at the experiment node and not receive any emails.

<img alt="Experiment without a control" />

## Editing experiments

Once you have started your workflow you can edit the number of contacts passing through each variant and control by first [pausing the workflow](/docs/workflows/pausing) from the header of the inspector panel on the right-hand side, or from the top nav on mobile.

Once you have paused the workflow, you can change the sample size, edit your variants and controls, and update any content.

Click **Resume** to start the workflow again.

<Info>
  When paused, email sending is stopped and contacts are queued behind their respective nodes as expected.
  All contacts that were scheduled to receive an email during the pause will receive it once you resume the workflow.
</Info>

### Closing an experiment

After some time testing, you may want to close experiment branches, to send all future contacts down your best-performing branch.

To do this while retaining the metrics for all branches, first change your best-performing branch to a control branch (click its **Use as control** toggle; see image above).

Then set the Experiment node sample size to 0%. This will change the Control size to 100%, sending all contacts down the new control branch.

<img alt="Close an experiment branch" />

## Results

Once your experiment is running and contacts have flowed through the workflow, you can see the results by clicking **Metrics** in the navigation panel on the left-hand side.

Here you can compare your variants and view metrics for sends, opens and clicks.


# Pausing workflows
Source: https://loops.so/docs/workflows/pausing

Pause or stop a Loops workflow to control email sending, manage timers in paused workflows, and resume or end automations safely.

## Pausing vs Stopping

Both pausing and stopping a workflow will prevent any emails from sending until you resume the workflow, but there are a few key differences.

<Tip>
  We generally recommend stopping emails based on in-app actions completed (e.g., “user upgraded,” “ticket closed,” “trial converted”) rather than on email replies as a reply (such as an out of office email) doesn't necessarily mean the action you emailed about has been taken.
</Tip>

## Pausing a workflow

When you pause a workflow, you can resume the workflow at any time.

During the pause, all contacts that were scheduled to receive an email will receive it once you resume the workflow. However, this assumes that these contacts still meet the necessary criteria to receive the email (e.g., they are subscribed and match any audience filters or other conditions you've set).

New contacts that match the Trigger conditions while the workflow is paused will enter the workflow but only within the first 24 hours. Contacts that qualify for the Trigger after the 24-hour mark will not enter the workflow.

<Tip>
  We will notify you via email once the 24-hour period has elapsed, reminding
  you that contacts will no longer queue to enter the workflow if you resume it.
</Tip>

The 24-hour limitation prevents contacts from entering a workflow that has been paused for an extended period and receiving irrelevant emails. For instance, if your workflow sends a welcome email to new contacts, you wouldn't want a contact to join the workflow three months after signing up and receive a welcome email.

## Timers in paused workflows

Contacts waiting on a timer node keep their original scheduled send time when you pause a workflow. If you resume before that time passes, the email sends at the originally scheduled time.

If the workflow is still paused when a contact's scheduled send time passes, they will receive the email shortly after you resume the workflow.

If you want to update the time for all queued contacts on a timer (A), you can create a new timer (B) after it and then delete the old timer (A), moving contacts to the new timer (B).

## Stopping a workflow

Stopping a workflow will not queue any new contacts to enter the workflow. Any contacts that were queued to enter the workflow will not enter the workflow. That's the key difference between pausing and stopping a workflow.


# Triggering workflows
Source: https://loops.so/docs/workflows/triggers

Learn how to trigger a workflow to start sending emails.

## Workflow triggers

A workflow trigger is an event, contact update or contact addition that starts a workflow. For example, if you create a workflow that sends a welcome email to new contacts, the trigger would be when a new contact is added to your audience.

## Different types of workflow triggers

There are currently four types of triggers that you can use to start a workflow: **Contact added**, **Contact updated**, **Contact added to list** and **Event received**.

### Contact added

The **Contact added** trigger will start a workflow when a contact is added to your audience. This trigger is useful for sending welcome emails to new contacts or sending a series of onboarding emails to new customers. This trigger works for contacts added via an integration, a form, or an API call. Contacts uploaded via CSV or added individually to the audience table will not be included. As long as the contact is added to your audience with an automatic method the workflow will start.

This trigger requires no additional setup. Once you create a workflow with this trigger, you can start adding contacts to your audience and contacts will enter the workflow.

### Contact updated

The **Contact updated** trigger will start a workflow when a contact is updated in your audience. This trigger is useful for sending emails to contacts based on their actions or behavior. For example, you can send a series of emails to contacts who change their subscription plan from free to paid or from paid to canceled.

You can also set the trigger to only start the workflow when a specific field is updated from a specific value to another specific value. For example, you can send a series of emails to contacts who change their subscription plan from free to paid but exclude contacts who have updated their subscription plan from paid to canceled.

### Contact added to list

The **Contact added to list** trigger will start a workflow when a contact is added to a [mailing list](/docs/contacts/mailing-lists). It triggers every time a contact is added to a list (so if the contact is removed from a list and then re-added, it will trigger again).

<Warning>
  This trigger is tied directly to a contact's subscription to the selected mailing list. If the contact is removed from the mailing list, they will be removed from any connected workflows when they reach the next node.
</Warning>

### Event received

The **Event received** trigger will start a workflow when a contact receives a specific matching event sent via API, Integrations or a form. This trigger is great for events like payment received, order placed, or a new message received. You can fire events with the [Events API](/docs/api-reference/send-event) or an integration.

## Trigger frequency

You can choose to trigger a workflow just the first time a contact matches the trigger settings or every time the contact matches.

<img alt="Trigger frequency" />

For example, if you want to send to a contact every time they update their subscription plan, you can choose to trigger the workflow every time the contact is updated (select "Every time").

However, if you want to send a welcome email to a contact just the first time they are added to your audience, you can choose to trigger the workflow once when the contact is added (select "One time").

## Changing the trigger type

You can change the trigger type at any time. For example, if you create a workflow with the Contact Added trigger, you can change it to the Contact Updated trigger at any time.

## Re-triggering workflows

Sometimes you may want to re-trigger a workflow for certain contacts, for example if you want to add an older contact to a new email sequence.

The easiest way to do this is to [download contacts in a CSV](/docs/contacts/export-contacts) and re-upload them with the [CSV uploader](/docs/add-users/csv-upload#trigger-workflows-via-csv). Make sure to toggle **Trigger workflows** in the last upload step.