# 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**.
When you are billed, your charge is always calculated based on the number of contacts you have at that exact time. If your contact list grows between billing cycles, you'll be notified via email and moved to the appropriate plan tier at your next billing date.
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 every 30 days (marketing and transactional combined).
* Loops branding is placed at the bottom of your emails.
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).
## 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).
## 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 [support@loops.so](mailto:support@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.
# 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.
The bot must be invited to private channels before they become visible in the dropdown.
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.
It is free to add additional team members to your account.
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.
Owners can 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.
The existing [team member features](/docs/account/team-members) allow you to invite and remove users to and from individual teams.
Free users can be in maximum 3 teams. As soon as a user joins a paid team, they can be added to unlimited teams.
## 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.
Make sure you're signed in to the Loops account you want to be your only account (Team A).
**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).
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**.
Accepting the invite will automatically switch you to the Team A account.
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.
You can sign out of Team B (or close your incognito window), because you can now access it through your Team A account.
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).
## 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.
Please read through the [contact properties](/docs/contacts/properties) information to understand how to format data in your CSV file.
## Add new contacts via CSV
1. Select **Import** in the top right of the [Audience](https://app.loops.so/audience) page.
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.
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.
## 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.
# 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
Find out how to send events using our API.
# API key
Source: https://loops.so/docs/api-reference/api-key
GET /v1/api-key
Test that an API key is valid.
## Request
No parameters.
## Response
### Success
The name of the team the API key belongs to.
### Error
A `401 Unauthorized` will be returned if the API key is invalid.
Deprecated fields will be removed in the future so avoid using them in your code.
"Invalid API key"
"Invalid API key"
```json Response theme={"dark"}
{
"success": true,
"teamName": "My team"
}
```
```json 401 response theme={"dark"}
{
"success": false,
"message": "Invalid API key",
"error": "Invalid API key"
}
```
# API Changelog
Source: https://loops.so/docs/api-reference/changelog
Stay up-to-date with changes to our API and webhooks.
New: endpoints for creating and editing transactional emails. These endpoints are currently in an open alpha.
New: [Upload](/docs/api-reference/create-upload) endpoints for uploading images to use in emails.
New: endpoints for creating and editing campaigns. These endpoints are currently in an open alpha.
New: API endpoints to [check](/docs/api-reference/check-contact-suppression) and [remove](/docs/api-reference/remove-contact-suppression) suppression for contacts.
New: a `campaignName` field in the [campaign.email.sent](/docs/webhooks#campaign-email-sent) webhook event.
New: an [`email.resubscribed`](/docs/webhooks#email-resubscribed) webhook event for
when contacts resubscribe to your marketing email from an email's
"Unsubscribe" link.
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).
New: [webhooks](/docs/webhooks) are now available to all users.
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.
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.
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)
New: an `Idempotency-Key` header when [sending
events](/docs/api-reference/send-event).
New: an `Idempotency-Key` header when [sending transactional
emails](/docs/api-reference/send-transactional-email).
New: an endpoint for [listing transactional
emails](/docs/api-reference/list-transactional-emails) and their data variables.
New: [webhooks](/docs/webhooks) are in Beta.
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.
New: we added `description` to mailing list objects in the [List mailing
lists](/docs/api-reference/list-mailing-lists) endpoint.
Improvement: the maximum payload size for the transactional endpoint has been
increased from 1MB to 4MB, allowing for more or larger attachments.
New: our new [Nuxt module](/docs/sdks/nuxt) is now out!
New: our new [Ruby SDK](/docs/sdks/ruby) is available!
New: we added an `isPublic` attribute to mailing list objects in the [List
mailing lists](/docs/api-reference/list-mailing-lists) endpoint.
New: there's a new `addToAudience` parameter in the [Send transactional
email](/docs/api-reference/send-transactional-email) endpoint can add contacts to
your audience.
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.
New: you can now [Find contacts](/docs/api-reference/find-contact) by `userId`.
New: a new endpoint for testing your integration and/or API key: [API
key](/docs/api-reference/api-key).
New: you can now include [event properties](/docs/events/properties) in requests to
the [Send event](/docs/api-reference/send-event) endpoint.
Improvement: we removed behavior that returned a `400 Bad Request` response if
an unrecognized property was added to Contact endpoints.
Improvement: sending in a payload that contains an unrecognized property will
now return a `400 Bad Request` response.
Clarification in the API docs that `userId` can be used in a [Send
event](/docs/api-reference/send-event) request.
New: we added a new endpoint for [querying custom contact
properties](/docs/api-reference/list-custom-fields).
Our [official JavaScript/TypeScript SDK](/docs/sdks/javascript) is now available!
New: we now accept dates as a custom [contact property](/docs/contacts/properties)
type. [View the available formats](/docs/contacts/properties#dates).
# Check contact suppression status
Source: https://loops.so/docs/api-reference/check-contact-suppression
GET /v1/contacts/suppression
Check if a contact is suppressed by email address or user ID.
## Request
### Query parameters
Check by email or user ID. Only one parameter is allowed.
The contact's email address. Make sure it is
[URI-encoded](https://en.wikipedia.org/wiki/Percent-encoding).
The contact's unique user ID.
## Response
### Success
This endpoint will return the suppression status for a contact and the suppression removal quota.
The contact's Loops-assigned ID.
The contact's email address.
The contact's unique user ID.
Whether the contact is suppressed.
The number of suppression removals you can request in a rolling 30 day period.
The remaining number of suppression removals left in the current 30 day period.
### Error
If there is an issue with the request, a `400 Bad Request` will be returned.
If no contact is found, a `404 Not Found` will be returned.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"contact": {
"id": "cll6b3i8901a9jx0oyktl2m4u",
"email": "test@example.com",
"userId": null
},
"isSuppressed": true,
"removalQuota": {
"limit": 0,
"remaining": 0
}
}
```
```json 400 response theme={"dark"}
{
"success": false,
"message": "An email or userId is required."
}
```
```json 404 response theme={"dark"}
{
"success": false,
"message": "This contact was not found."
}
```
# Complete an upload
Source: https://loops.so/docs/api-reference/complete-upload
POST /v1/uploads/{id}/complete
Finalize an image upload after the file has been uploaded to the pre-signed URL.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
Call this endpoint after uploading the file to the `presignedUrl` returned by
[Create an upload](/docs/api-reference/create-upload).
### Path parameters
The `emailAssetId` returned when the upload was created.
## Response
### Success
The ID of the uploaded asset.
The public URL of the uploaded asset. Use this URL in [LMX image elements](/docs/creating-emails/lmx#images) when
[updating an email message](/docs/api-reference/update-email-message).
### Error
A `400 Bad Request` is returned if the upload id is missing or the uploaded
file has an unsupported content type.
A `404 Not Found` is returned if the upload does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"emailAssetId": "cmp2cz4mn0oru0izztre5fgob",
"finalUrl": "https://images.vialoops.com/cmp2cnlf600yz0f042m792otk/cmp2cz4mn0oru0izztre5fgob.jpg"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Upload not found."
}
```
# Create a campaign
Source: https://loops.so/docs/api-reference/create-campaign
POST /v1/campaigns
Create a new draft campaign and associated email message.
Content API endpoints are currently in an open alpha and are subject to
change.
This endpoint creates a draft campaign and an empty email message in one step.
Use the returned `emailMessageId` with
[Update an email message](/docs/api-reference/update-email-message) to set
subject, sender, preview text, and LMX content.
## Request
### Body
The campaign name.
## Response
### Success
The campaign ID.
The campaign name.
The initial campaign status (`Draft`).
ISO 8601 timestamp for when the campaign was created.
ISO 8601 timestamp for when the campaign was last updated.
The ID of the empty [email message](/docs/api-reference/update-email-message) created for this campaign.
The initial content revision ID for the email message. Pass this as
`expectedRevisionId` on your first [email message update](/docs/api-reference/update-email-message).
### Error
If the request body is invalid, or if no sending domain is configured, a
`400 Bad Request` is returned.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"name": "Spring announcement",
"status": "Draft",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z",
"emailMessageId": "cmn5zia4i0017tzli8ric8giv",
"emailMessageContentRevisionId": "rev_01hxyz"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "No sending domain configured."
}
```
# Create contact
Source: https://loops.so/docs/api-reference/create-contact
POST /v1/contacts/create
Create a new contact with an email address and any other contact properties.
If you want to "update or create" contacts, consider using the [Update a
contact](/docs/api-reference/update-contact) endpoint instead.
## Request
### Body
The contact's email address.
The contact's first name.
The contact's last name.
A custom source value to replace the default "API". [Read
more](/docs/contacts/properties#source)
Whether the contact will receive campaign and workflow emails. [Read
more](/docs/contacts/properties#subscribed)
We recommend leaving this field out of your requests unless you specifically want to unsubscribe or re-subscribe a contact. All new contacts are subscribed by default.
You can use groups to segment users when sending emails. Currently, a contact
can only be in one user group. [Read more](/docs/contacts/properties#user-group)
A unique user ID (for example, from an external application). [Read
more](/docs/contacts/properties#user-id)
Manage mailing list subscriptions.\
Include key-value pairs of mailing list IDs and a `boolean` denoting if the contact
should be added (`true`) or removed (`false`) from the list. [Read
more](/docs/contacts/mailing-lists#add-contacts-to-lists-with-the-api)
```json theme={"dark"}
"mailingLists": {
"cm06f5v0e45nf0ml5754o9cix": true,
"cm16k73gq014h0mmj5b6jdi9r": false
}
```
### Custom properties
You can also include [custom contact properties](/docs/contacts/properties) in your request body. These should be added as top-level attributes in the request.
Custom properties can be of type `string`, `number`, `boolean` or `date` ([see allowed date formats](/docs/contacts/properties#dates)).
```json theme={"dark"}
{
"email": "test@example.com",
"plan": "pro" /* Custom property */,
"dateJoined": 1704711066 /* Custom property */
}
```
There are a few [reserved names](/docs/contacts/properties#reserved-names) that you
cannot use for custom properties.
To empty or reset the value of a contact property, send a `null` value.
## Response
### Success
The internal ID of the new contact.
### Error
If a matching contact already exists in your audience, a `409 Conflict` error will be returned. All other errors will be `400 Bad Request`.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true,
"id": "id_of_contact"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message"
}
```
# Create contact property
Source: https://loops.so/docs/api-reference/create-contact-property
POST /v1/contacts/properties
Create a new contact property.
## Request
### Body
The name of the property.\
This should be in `camelCase`, like `planName` or `favoriteColor`.
The property's value type.
Allowed values:
* `string`
* `number`
* `boolean`
* `date`
There are a few [reserved names](/docs/contacts/properties#reserved-names) that you
cannot use for contact properties.
## Response
### Success
### Error
Errors will be `400 Bad Request`.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
# Create a transactional email
Source: https://loops.so/docs/api-reference/create-transactional-email
POST /v1/transactional-emails
Create a new transactional and associated draft email message.
Content API endpoints are currently in an open alpha and are subject to
change.
This endpoint creates a transactional email and an empty draft email message in one step.
Use the returned `draftEmailMessageId` and `draftEmailMessageContentRevisionId` when calling
[Update an email message](/docs/api-reference/update-email-message) to set subject,
sender, preview text, and LMX content. Then call
[Publish a transactional email](/docs/api-reference/publish-transactional-email) when you're ready to start sending.
## Request
### Body
The transactional email name.
## Response
### Success
Returns `201 Created`.
The transactional email ID.
The transactional email name.
The ID of the draft [email message](/docs/api-reference/update-email-message)
created for this transactional email.
The initial content revision ID for the draft email message. Pass this as
`expectedRevisionId` on your first [email message update](/docs/api-reference/update-email-message).
The ID of the published email message, if one exists.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
If the request body is invalid, or if no sending domain is configured, a
`400 Bad Request` is returned.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email",
"draftEmailMessageId": "cmn5zia4i0017tzli8ric8giv",
"draftEmailMessageContentRevisionId": "rev_01hxyz",
"publishedEmailMessageId": null,
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z",
"dataVariables": []
}
```
```json Error response theme={"dark"}
{
"message": "No sending domain configured."
}
```
# Create an upload
Source: https://loops.so/docs/api-reference/create-upload
POST /v1/uploads
Request a pre-signed URL to upload an image asset.
Content API endpoints are currently in an open alpha and are subject to
change.
Request a pre-signed URL to upload an image asset. Upload the file with an HTTP
`PUT` to the returned `presignedUrl` (sending the same `Content-Type` and
`Content-Length`), then call
[Complete an upload](/docs/api-reference/complete-upload) to finalize the asset.
## Request
### Body
The MIME type of the file to upload. Supported types are `image/jpeg`,
`image/png`, `image/gif`, and `image/webp`.
The size of the file in bytes. Must be a positive integer no greater than
4,000,000 bytes.
## Response
### Success
The ID of the created asset. Pass this as `id` to
[Complete an upload](/docs/api-reference/complete-upload) once the file has been
uploaded.
The pre-signed URL to upload the file to with an HTTP `PUT` request. Send the
same `Content-Type` and `Content-Length` used in the create request.
### Error
If the request body is invalid, or if `contentType` is unsupported, a
`400 Bad Request` is returned. The response may include a
`supportedContentTypes` array listing accepted MIME types.
If the upload exceeds the maximum allowed size, a `413 Payload Too Large` is
returned. The response may include a `maxBytes` field with the size limit.
If the upload limit is exceeded, a `429 Too Many Requests` is returned. The
response may include `maxUploads` and `windowHours` fields describing the
limit window.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
Present when the request was rejected for an unsupported `contentType`. Lists
the accepted MIME types.
Present when the upload exceeds the size limit. The maximum allowed size in
bytes.
Present when the upload limit is exceeded. The maximum number of uploads
allowed per window.
Present when the upload limit is exceeded. The number of hours in the upload
limit window.
```json Response theme={"dark"}
{
"success": true,
"emailAssetId": "asset_01hxyz",
"presignedUrl": "https://storage.example.com/upload?signature=..."
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Unsupported contentType.",
"supportedContentTypes": [
"image/jpeg",
"image/png",
"image/gif",
"image/webp"
]
}
```
```json Upload limit exceeded theme={"dark"}
{
"success": false,
"message": "Upload limit exceeded: max 50 uploads per 24 hours. Please contact support if you need to increase your upload limit.",
"maxUploads": 50,
"windowHours": 24
}
```
# List dedicated sending IP addresses
Source: https://loops.so/docs/api-reference/dedicated-sending-ips
GET /v1/dedicated-sending-ips
Retrieve a list of Loops' dedicated sending IP addresses.
This endpoint is provided for the rare instances where you may need to whitelist our sending IPs. Please note that this list is subject to change and will not include shared IPs used for sending mail.
Unless you are sure you need this and are comfortable watching for changes, we strongly recommend you *do not* whitelist these IPs.
This endpoint may be rate-limited.
## Request
No parameters.
## Response
Returns an array of IP address strings.
IP address (e.g., "1.2.3.4")
If there are no dedicated IP addresses an empty array is returned.
### Error
A `405` error will be returned if the wrong HTTP request method is used.
Error message
```json Response theme={"dark"}
[
"1.2.3.4"
]
```
# Delete contact
Source: https://loops.so/docs/api-reference/delete-contact
POST /v1/contacts/delete
Delete a contact by email address or user ID.
## Request
### Body
You can delete a contact by using either their `email` or `userId` value.
The contact's email address.
The contact's `userId` value.
## Response
### Success
"Contact deleted."
### Error
If a matching contact is not found, a `404 Not Found` will be returned. All other errors will be `400 Bad Request`.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true,
"message": "Contact deleted."
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
# Ensure a transactional email has a draft
Source: https://loops.so/docs/api-reference/ensure-transactional-draft
POST /v1/transactional-emails/{transactionalId}/draft
Ensure a transactional email has a draft email message for editing.
Content API endpoints are currently in an open alpha and are subject to
change.
If a draft email message already exists, it is returned unchanged. Otherwise a
new empty draft is created (seeded from the most recent published version when
present).
Use the returned `draftEmailMessageId` and `draftEmailMessageContentRevisionId` when calling [Update an email message](/docs/api-reference/update-email-message) to edit the draft's content.
## Request
### Path parameters
The ID of the transactional email.
### Body
No request body.
## Response
### Success
The transactional email ID.
The transactional email's name.
The ID of the draft [email message](/docs/api-reference/update-email-message).
The `contentRevisionId` of the draft email message. Pass this as
`expectedRevisionId` on your first update via
[Update an email message](/docs/api-reference/update-email-message).
The ID of the published email message, if one exists.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
If `transactionalId` is invalid, or if no sending domain is configured, a
`400 Bad Request` is returned.
A `404 Not Found` is returned if the transactional email does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email",
"draftEmailMessageId": "cmn5zia4i0017tzli8ric8giv",
"draftEmailMessageContentRevisionId": "rev_01hxyz",
"publishedEmailMessageId": "cmn5bia4i0217tzli8ric8giv",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:20:00.000Z",
"dataVariables": []
}
```
```json Error response theme={"dark"}
{
"message": "Transactional email not found."
}
```
# API Examples
Source: https://loops.so/docs/api-reference/examples
Code examples of how to use the Loops API and SDKs.
Code examples for managing contacts.
Code examples for creating campaigns and editing email content with LMX.
Code examples for sending events.
Code examples for sending and retrieving transactional emails.
# Campaigns API examples
Source: https://loops.so/docs/api-reference/examples/campaigns
Copy/paste code examples for creating campaigns, updating email messages using content revisions, and querying themes/components for LMX.
## Create a campaign
This creates a draft campaign and a related email message in one request.
Only a `name` value is required.
Save the returned `emailMessageContentRevisionId`. Pass it as
`expectedRevisionId` when updating an email message to avoid `409 Conflict` errors caused by stale revisions.
[API reference](/docs/api-reference/create-campaign)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/campaigns", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Spring product announcement",
}),
});
const data = await response.json();
const emailMessageId = data.emailMessageId;
const emailMessageContentRevisionId = data.emailMessageContentRevisionId;
```
```python Python theme={"dark"}
import requests
response = requests.post(
"https://app.loops.so/api/v1/campaigns",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
json={
"name": "Spring product announcement",
},
)
data = response.json()
email_message_id = data["emailMessageId"]
content_revision_id = data["emailMessageContentRevisionId"]
```
## Query themes and components for your LMX
You can fetch your available themes and reusable components before building
the `lmx` payload.
[List themes API reference](/docs/api-reference/list-themes)\
[List components API reference](/docs/api-reference/list-components)
```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 ",
},
}),
fetch("https://app.loops.so/api/v1/components?perPage=20", {
method: "GET",
headers: {
"Authorization": "Bearer ",
},
}),
]);
const themes = await themesResponse.json();
const components = await componentsResponse.json();
```
```python Python theme={"dark"}
import requests
themes_response = requests.get(
"https://app.loops.so/api/v1/themes",
headers={
"Authorization": "Bearer ",
},
params={"perPage": 20},
)
components_response = requests.get(
"https://app.loops.so/api/v1/components",
headers={
"Authorization": "Bearer ",
},
params={"perPage": 20},
)
themes = themes_response.json()
components = components_response.json()
```
## 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 ``, and create an email using LMX elements.
Themes and components you queried in step 2 can be referenced by their IDs.
Save the returned `contentRevisionId` after each update. Pass it as
`expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
by stale revisions.
[API reference](/docs/api-reference/update-email-message)\
[Get theme API reference](/docs/api-reference/get-theme)\
[Get component API reference](/docs/api-reference/get-component)
```js JavaScript theme={"dark"}
const lmxContent = `
Hey there, here is what's new.
...
`;
const response = await fetch(
`https://app.loops.so/api/v1/email-messages/${emailMessageId}`,
{
method: "POST",
headers: {
"Authorization": "Bearer ",
"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;
```
```python Python theme={"dark"}
import requests
response = requests.post(
f"https://app.loops.so/api/v1/email-messages/{email_message_id}",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
json={
"expectedRevisionId": email_message_content_revision_id,
"subject": "Big spring updates",
"previewText": "A quick look at what's new",
"fromName": "Loops",
"fromEmail": "hello",
"replyToEmail": "support@example.com",
"lmx": "Hey there, here is what's new.",
},
)
updated = response.json()
next_content_revision_id = updated["contentRevisionId"]
```
## Upload an image asset
If your LMX includes `` 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)
```js JavaScript theme={"dark"}
const createResponse = await fetch("https://app.loops.so/api/v1/uploads", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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 ",
},
},
);
const { finalUrl } = await completeResponse.json();
```
```python Python theme={"dark"}
import requests
create_response = requests.post(
"https://app.loops.so/api/v1/uploads",
headers={
"Authorization": "Bearer ",
"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 ",
},
)
final_url = complete_response.json()["finalUrl"]
```
# Contacts API examples
Source: https://loops.so/docs/api-reference/examples/contacts
Copy/paste code examples for managing contacts with the Loops API and SDKs, including create, update, find, and delete requests with sample payloads.
## Create a contact
[API reference](/docs/api-reference/create-contact)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/create", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.createContact(
"test@example.com",
{
firstName: "John",
lastName: "Doe"
},
);
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"firstName": "John",
"lastName": "Doe"
}
)
```
## Create a contact and add them to a mailing list
[API reference](/docs/api-reference/create-contact)
```js JavaScript {11-13} theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/create", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
firstName: "John",
lastName: "Doe",
mailingLists: {
"": true
},
}),
});
const data = await response.json();
```
```js JavaScript SDK {10-12,17} theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const properties = {
firstName: "John",
lastName: "Doe",
};
const mailingLists = {
"": true,
};
const response = await loops.createContact(
"test@example.com",
properties,
mailingLists,
);
```
```php PHP SDK {11-13} theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->contacts->create(
email: 'test@example.com',
properties: [
'firstName' => 'John',
'lastName' => 'Doe',
],
mailing_lists: [
'' => TRUE,
],
);
```
```ruby Ruby SDK {7-9} theme={"dark"}
response = LoopsSdk::Contacts.create(
email: "test@example.com",
properties: {
firstName: "John",
lastName: "Doe",
},
mailing_lists: {
"": true,
},
)
```
```python Python {13-15} theme={"dark"}
import requests
response = requests.post(
"https://app.loops.so/api/v1/contacts/create",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"firstName": "John",
"lastName": "Doe",
"mailingLists": {
"": True
}
}
)
```
## Update a contact
When updating a contact you must provide an `email` or `userId` value to identify the contact.
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.
[API reference](/docs/api-reference/update-contact)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
method: "PUT",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.updateContact(
"test@example.com",
{
planName: "Pro",
},
);
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"planName": "Pro"
}
)
```
## 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)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
method: "PUT",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.updateContact(
"test@example.com",
{
userId: "12345",
},
);
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"userId": "12345"
}
)
```
## Subscribe a contact to a mailing list
[API reference](/docs/api-reference/update-contact)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
method: "PUT",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json"
},
body: JSON.stringify({
email: "test@example.com",
mailingLists: {
"" => true
},
}),
});
const data = await response.json();
```
```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.updateContact(
"test@example.com",
{},
{
"": true,
},
);
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->contacts->update(
email: 'test@example.com',
mailing_lists: [
'' => TRUE,
],
);
```
```ruby Ruby SDK theme={"dark"}
response = LoopsSdk::Contacts.update(
email: "test@example.com",
mailing_lists: {
"" => true,
},
)
```
```python Python theme={"dark"}
import requests
response = requests.put(
"https://app.loops.so/api/v1/contacts/update",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"mailingLists": {
"": True
}
}
)
```
## 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)
```js JavaScript {9-11} theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
method: "PUT",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json"
},
body: JSON.stringify({
email: "test@example.com",
mailingLists: {
"" => false
},
}),
});
const data = await response.json();
```
```js JavaScript SDK {8-10} theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.updateContact(
"test@example.com",
{},
{
"" => false,
},
);
```
```php PHP SDK {7-9} theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->contacts->update(
email: 'test@example.com',
mailing_lists: [
'' => FALSE,
],
);
```
```ruby Ruby SDK {3-5} theme={"dark"}
response = LoopsSdk::Contacts.update(
email: "test@example.com",
mailing_lists: {
"" => false,
},
)
```
```python Python {11-13} theme={"dark"}
import requests
response = requests.put(
"https://app.loops.so/api/v1/contacts/update",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"mailingLists": {
"": False
}
}
)
```
## 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)
```js JavaScript {9} theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/update", {
method: "PUT",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.updateContact(
"test@example.com",
{
subscribed: false,
},
);
```
```php PHP SDK {7} theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"subscribed": False
}
)
```
## Delete a contact
You can delete contacts by email or user ID.
[API reference](/docs/api-reference/delete-contact)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/contacts/delete", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.deleteContact({
email: "test@example.com",
});
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
}
)
```
## Check contact suppression status
[API reference](/docs/api-reference/check-contact-suppression)
```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 ",
},
},
);
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 ",
},
params={
"email": "test@example.com",
},
)
data = response.json()
```
## Remove suppression for a contact
[API reference](/docs/api-reference/remove-contact-suppression)
```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 ",
},
},
);
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 ",
},
params={
"email": "test@example.com",
},
)
data = response.json()
```
# Events API examples
Source: https://loops.so/docs/api-reference/examples/events
Copy/paste code examples for sending events to Loops via API and SDKs to trigger workflows and attach event properties, with sample payloads.
## Send an event
[API reference](/docs/api-reference/send-event)
```js JavaScript theme={"dark"}
await fetch("https://app.loops.so/api/v1/events/send", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
const response = await loops.sendEvent({
email: "test@example.com",
eventName: "testEvent",
});
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"eventName": "testEvent",
}
)
```
## 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)
```js JavaScript {10-12} theme={"dark"}
await fetch("https://app.loops.so/api/v1/events/send", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
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("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"eventName": "testEvent",
"eventProperties": {
"testProperty": "testValue",
},
}
)
```
## 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)
```js JavaScript {10} theme={"dark"}
await fetch("https://app.loops.so/api/v1/events/send", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
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("");
$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 ",
"Content-Type": "application/json"
},
json={
"email": "test@example.com",
"eventName": "testEvent",
"contactProperties": {
"planName": "Pro",
},
}
)
```
## 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)
```js JavaScript {6} theme={"dark"}
await fetch("https://app.loops.so/api/v1/events/send", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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("");
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("");
$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 ",
"Content-Type": "application/json"
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
json={
"email": "test@example.com",
"eventName": "testEvent",
}
)
```
# Transactional email API examples
Source: https://loops.so/docs/api-reference/examples/transactional-emails
Copy/paste code examples for creating transactionals, updating draft email messages using content revisions, publishing, and sending transactional email with Loops via API 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.
Save the returned `draftEmailMessageContentRevisionId`. Pass it as
`expectedRevisionId` when updating the draft email message to avoid `409 Conflict` errors caused by stale revisions.
[API reference](/docs/api-reference/create-transactional-email)
```js JavaScript theme={"dark"}
const response = await fetch("https://app.loops.so/api/v1/transactional-emails", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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;
```
```python Python theme={"dark"}
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional-emails",
headers={
"Authorization": "Bearer ",
"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"
]
```
## 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)\
[List components API reference](/docs/api-reference/list-components)
```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 ",
},
}),
fetch("https://app.loops.so/api/v1/components?perPage=20", {
method: "GET",
headers: {
"Authorization": "Bearer ",
},
}),
]);
const themes = await themesResponse.json();
const components = await componentsResponse.json();
```
```python Python theme={"dark"}
import requests
themes_response = requests.get(
"https://app.loops.so/api/v1/themes",
headers={
"Authorization": "Bearer ",
},
params={"perPage": 20},
)
components_response = requests.get(
"https://app.loops.so/api/v1/components",
headers={
"Authorization": "Bearer ",
},
params={"perPage": 20},
)
themes = themes_response.json()
components = components_response.json()
```
## Update the draft email message with `contentRevisionId`
Use `draftEmailMessageId` from when you created the transactional as the path parameter, and pass `draftEmailMessageContentRevisionId` as `expectedRevisionId`.
Apply styles or a theme in ``, and build the email using LMX elements.
Themes and components you queried in the previous step can be referenced by their IDs.
Save the returned `contentRevisionId` after each update. Pass it as
`expectedRevisionId` on the next update to avoid `409 Conflict` errors caused
by stale revisions.
[API reference](/docs/api-reference/update-email-message)\
[Get theme API reference](/docs/api-reference/get-theme)\
[Get component API reference](/docs/api-reference/get-component)
```js JavaScript theme={"dark"}
const lmxContent = `
Click the link below to reset your password.
...
`;
const response = await fetch(
`https://app.loops.so/api/v1/email-messages/${draftEmailMessageId}`,
{
method: "POST",
headers: {
"Authorization": "Bearer ",
"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;
```
```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 ",
"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": "Click the link below to reset your password.",
},
)
updated = response.json()
next_content_revision_id = updated["contentRevisionId"]
```
## Upload an image asset
If your LMX includes `` 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)
```js JavaScript theme={"dark"}
const createResponse = await fetch("https://app.loops.so/api/v1/uploads", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"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 ",
},
},
);
const { finalUrl } = await completeResponse.json();
```
```python Python theme={"dark"}
import requests
create_response = requests.post(
"https://app.loops.so/api/v1/uploads",
headers={
"Authorization": "Bearer ",
"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 ",
},
)
final_url = complete_response.json()["finalUrl"]
```
## Publish the transactional email
Publish the draft email message so it can be sent with the transactional send endpoint.
[API reference](/docs/api-reference/publish-transactional-email)
```js JavaScript theme={"dark"}
const publishResponse = await fetch(
`https://app.loops.so/api/v1/transactional-emails/${transactionalId}/publish`,
{
method: "POST",
headers: {
"Authorization": "Bearer ",
},
},
);
const published = await publishResponse.json();
```
```python Python theme={"dark"}
import requests
publish_response = requests.post(
f"https://app.loops.so/api/v1/transactional-emails/{transactional_id}/publish",
headers={
"Authorization": "Bearer ",
},
)
published = publish_response.json()
```
## 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)
```js JavaScript theme={"dark"}
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: transactionalId,
dataVariables: {
loginUrl: "https://example.com/login",
},
}),
});
```
```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "",
dataVariables: {
loginUrl: "https://example.com/login",
},
});
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_id: '',
data_variables: [
'loginUrl' => 'https://example.com/login',
],
);
```
```ruby Ruby SDK theme={"dark"}
response = LoopsSdk::Transactional.send(
email: "test@example.com",
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 ",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": transactional_id,
"dataVariables": {
"loginUrl": "https://example.com/login",
},
},
)
```
## 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)
```js JavaScript {11-14} theme={"dark"}
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "",
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("");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "",
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("");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_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: "",
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 ",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": "",
"dataVariables": {
"items": [
{ "name": "Item 1", "description": "Description of Item 1" },
{ "name": "Item 2", "description": "Description of Item 2" },
],
},
},
})
```
## Send a transactional email with attachments
You must request attachments to be enabled in your account before you can send emails with them.
[API reference](/docs/api-reference/send-transactional-email#param-attachments)
```js JavaScript {13-19} theme={"dark"}
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "",
dataVariables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: "example.pdf",
contentType: "application/pdf",
data: "",
},
],
}),
});
```
```js JavaScript SDK {11-17} theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "",
dataVariables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: "example.pdf",
contentType: "application/pdf",
data: "",
},
],
});
```
```php PHP SDK {11-17} theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_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: "",
data_variables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: 'example.pdf',
content_type: 'application/pdf',
data: '',
},
],
)
```
```python Python {15-21} theme={"dark"}
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": "",
"dataVariables": {
"loginUrl": "https://example.com/login",
},
"attachments": [
{
"filename": "example.pdf",
"contentType": "application/pdf",
"data": "",
},
],
},
)
```
## 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)
```js JavaScript {6} theme={"dark"}
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer ",
"Content-Type": "application/json",
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "",
dataVariables: {
loginUrl: "https://example.com/login",
},
}),
});
```
```js JavaScript SDK {11-13} theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "",
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("");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_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: "",
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 ",
"Content-Type": "application/json",
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
json={
"email": "test@example.com",
"transactionalId": "",
"dataVariables": {
"loginUrl": "https://example.com/login",
},
},
)
```
## List transactional emails
[API reference](/docs/api-reference/list-transactional-emails)
```js JavaScript theme={"dark"}
await fetch("https://app.loops.so/api/v1/transactional-emails", {
method: "GET",
headers: {
"Authorization": "Bearer ",
},
});
```
```js JavaScript SDK theme={"dark"}
import { LoopsClient } from "loops";
const loops = new LoopsClient("");
const response = await loops.getTransactionalEmails();
```
```php PHP SDK theme={"dark"}
use Loops\LoopsClient;
$loops = new LoopsClient("");
$result = $loops->transactional->get();
```
```ruby Ruby SDK theme={"dark"}
response = LoopsSdk::Transactional.list(perPage: 50)
```
```python Python theme={"dark"}
import requests
response = requests.get(
"https://app.loops.so/api/v1/transactional-emails",
headers={
"Authorization": "Bearer ",
},
)
```
# Find contact
Source: https://loops.so/docs/api-reference/find-contact
GET /v1/contacts/find
Find a contact by email address or user ID.
## Request
### Query parameters
Search by email or user ID. Only one parameter is allowed.
The contact's email address. Make sure it is
[URI-encoded](https://en.wikipedia.org/wiki/Percent-encoding).
The contact's unique user ID.
## Response
This endpoint will return a list of contact objects containing all default properties and any [custom properties](/docs/contacts/properties).
If no contact is found, an empty list will be returned.
The contact's Loops-assigned ID.
The contact's email address.
The contact's first name.
The contact's last name.
The source the contact was created from.
Whether the contact will receive campaign and workflow emails.
The contact's user group.
The contact's unique user ID.
Mailing lists the contact is subscribed to, represented by key-value pairs of mailing list IDs and `true`.
The contact's [double opt-in](/docs/contacts/double-opt-in) status.\
One of: `"pending"`, `"accepted"`, `"rejected"` or `null`.
This will be `null` for contacts unless they are created via a form while double opt-in is enabled.
```json Response theme={"dark"}
[
{
"id": "cll6b3i8901a9jx0oyktl2m4u",
"email": "test@example.com",
"firstName": "Bob",
"lastName": null,
"source": "API",
"subscribed": true,
"userGroup": "",
"userId": null,
"mailingLists": {
"cm06f5v0e45nf0ml5754o9cix": true
},
"optInStatus": "accepted"
}
]
```
```json No contact found response theme={"dark"}
[]
```
# Get a campaign
Source: https://loops.so/docs/api-reference/get-campaign
GET /v1/campaigns/{campaignId}
Retrieve a single campaign by ID.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the campaign.
## Response
### Success
The campaign ID.
The campaign name.
The campaign status. One of `Draft`, `Scheduled`, `Sending`, `Sent`.
ISO 8601 timestamp for when the campaign was created.
ISO 8601 timestamp for when the campaign was last updated.
The associated email message ID.
### Error
A `400 Bad Request` is returned if `campaignId` is invalid.
A `404 Not Found` is returned if the campaign does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"name": "Spring announcement",
"status": "Draft",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:10:00.000Z",
"emailMessageId": "cmn5zia4i0017tzli8ric8giv"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Campaign not found."
}
```
# Get a component
Source: https://loops.so/docs/api-reference/get-component
GET /v1/components/{componentId}
Retrieve a single component by ID.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the component.
## Response
### Success
The component ID.
The component name.
The component body serialized as LMX.
### Error
A `400 Bad Request` is returned if `componentId` is invalid.
A `404 Not Found` is returned if the component does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
```json Response theme={"dark"}
{
"success": true,
"componentId": "cmn5ziajr01oztzlifwarda8m",
"name": "Hero header",
"lmx": "Welcome"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Component not found."
}
```
# Get an email message
Source: https://loops.so/docs/api-reference/get-email-message
GET /v1/email-messages/{emailMessageId}
Retrieve an email message, including its compiled LMX content.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the email message.
## Response
### Success
The email message ID.
The ID of the campaign that owns this message.
Email subject line.
Email preview text.
Sender display name.
Sender email address.
Reply-to email address.
The email body serialized as [LMX](/docs/creating-emails/lmx).
The current content revision ID. Pass this as `expectedRevisionId` when
updating the message.
ISO 8601 timestamp for when the message was last updated.
### Error
A `400 Bad Request` is returned for invalid `emailMessageId`, or if no sending
domain is configured.
A `404 Not Found` is returned if the email message does not exist.
A `409 Conflict` is returned when the email message uses MJML format or content cannot be parsed.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"emailMessageId": "cmn5zia4i0017tzli8ric8giv",
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"subject": "Big spring updates",
"previewText": "A quick look at what's new",
"fromName": "Loops",
"fromEmail": "hello@news.example.com",
"replyToEmail": "support@example.com",
"lmx": "Hello world",
"contentRevisionId": "rev_01hxyz",
"updatedAt": "2026-03-28T15:20:00.000Z"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Email message not found."
}
```
# Get a theme
Source: https://loops.so/docs/api-reference/get-theme
GET /v1/themes/{themeId}
Retrieve a single theme by ID.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the theme.
## Response
### Success
The theme ID.
The theme name.
The theme's style attributes. Attributes use the same names as the LMX [``](/docs/creating-emails/lmx#document-styles) tag attributes.
Whether this theme is the team's default theme.
ISO 8601 timestamp for when the theme was created.
ISO 8601 timestamp for when the theme was last updated.
### Error
A `400 Bad Request` is returned if `themeId` is invalid.
A `404 Not Found` is returned if the theme does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
```json Response theme={"dark"}
{
"success": true,
"themeId": "thm_01hxyz",
"name": "Marketing default",
"styles": {
"backgroundColor": "#ffffff",
"textBaseColor": "#111111",
"textBaseFontSize": 16
},
"isDefault": true,
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Theme not found."
}
```
# Get a transactional email
Source: https://loops.so/docs/api-reference/get-transactional-email
GET /v1/transactional-emails/{transactionalId}
Retrieve a single transactional email by ID.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the transactional email.
## Response
### Success
The transactional email ID.
The transactional email name.
The ID of the draft email message, if one exists.
The ID of the published email message, if one exists.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
A `400 Bad Request` is returned if `transactionalId` is invalid.
A `404 Not Found` is returned if the transactional email does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email",
"draftEmailMessageId": "cmn5zia4i0017tzli8ric8giv",
"publishedEmailMessageId": "cmn5bia4i0217tzli8ric8giv",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:10:00.000Z",
"dataVariables": [
"firstName",
"lastName"
]
}
```
```json Error response theme={"dark"}
{
"message": "Transactional email not found."
}
```
# 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
Your Loops API key should never be used client side or exposed to your end
users.
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**.
This creates an API key. You can assign it a human-readable name:
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:
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"
```
## API Reference
The base URL for the API is `https://app.loops.so/api`
### API key
Test your API key.
### Contacts
Manage contacts.
### Contact properties
Manage contact properties.
### Mailing lists
View your mailing lists.
### Campaigns
Create and manage campaigns.
### Email messages
Email messages are the content of a campaign, loop or transactional email.
### Uploads
Upload images for email messages.
### Themes
Retrieve email themes.
### Components
Reusable components for your emails.
### Events
Send events to trigger workflows.
### Transactional emails
Send a transactional email.
### Configuration
## 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**.
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:
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:
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.
## Debugging
Sometimes things go wrong. Here are some tips to help you debug your API requests.
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.
## 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.
The official Go SDK for Loops.
The official JavaScript/TypeScript SDK for Loops.
} href="/sdks/nuxt">
The official Nuxt module for Loops.
The official PHP SDK for Loops.
} href="/sdks/ruby">
The official Ruby SDK for Loops.
### 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 campaigns
Source: https://loops.so/docs/api-reference/list-campaigns
GET /v1/campaigns
Retrieve a paginated list of campaigns.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Query parameters
How many results to return in each request. Must be between 10 and 50.
A cursor to return a specific page of results. Cursors can be found from the
`pagination.nextCursor` value in each response.
## Response
### Success
Total results found.
The number of results returned in this response.
The maximum number of results requested.
Total number of pages.
The next cursor (for retrieving the next page of results using the `cursor`
parameter), or `null` if there are no further pages.
The URL of the next page of results, or `null` if there are no further
pages.
The campaign ID.
The associated email message ID.
The campaign name.
The campaign subject line.
Campaign lifecycle status (for example `Draft`, `Scheduled`, `Sending`,
`Sent`).
ISO 8601 timestamp for when the campaign was created.
ISO 8601 timestamp for when the campaign was last updated.
### Error
If `perPage` is invalid, a `400 Bad Request` is returned.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"pagination": {
"totalResults": 2,
"returnedResults": 2,
"perPage": 20,
"totalPages": 1,
"nextCursor": null,
"nextPage": null
},
"data": [
{
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"emailMessageId": "cmn5zia4i0017tzli8ric8giv",
"name": "Spring announcement",
"subject": "Big spring updates",
"status": "Draft",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z"
}
]
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Invalid perPage value"
}
```
# List components
Source: https://loops.so/docs/api-reference/list-components
GET /v1/components
Retrieve a paginated list of email components.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Query parameters
How many results to return in each request. Must be between 10 and 50.
A cursor to return a specific page of results. Cursors can be found from the
`pagination.nextCursor` value in each response.
## Response
### Success
Total results found.
The number of results returned in this response.
The maximum number of results requested.
Total number of pages.
The next cursor (for retrieving the next page of results using the `cursor`
parameter), or `null` if there are no further pages.
The URL of the next page of results, or `null` if there are no further
pages.
### Error
A `400 Bad Request` is returned if `perPage` is invalid.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
```json Response theme={"dark"}
{
"success": true,
"pagination": {
"totalResults": 1,
"returnedResults": 1,
"perPage": 20,
"totalPages": 1,
"nextCursor": null,
"nextPage": null
},
"data": [
{
"componentId": "cmn5ziajr01oztzlifwarda8m",
"name": "Hero header",
"lmx": "Welcome"
}
]
}
```
# List contact properties
Source: https://loops.so/docs/api-reference/list-contact-properties
GET /v1/contacts/properties
Retrieve a list of your account's contact properties.
## Request
### Query parameters
`"all"` (default) or `"custom"`. Use `"custom"` to only list your team's custom properties.
## Response
This endpoint will return a list of contact property objects.
The property's name key.
The human-friendly label for this property.
The type of property (one of `string`, `number`, `boolean` or `date`).
```json Response theme={"dark"}
[
{
"key": "firstName",
"label": "First Name",
"type": "string"
},
{
"key": "lastName",
"label": "Last Name",
"type": "string"
},
{
"key": "email",
"label": "Email",
"type": "string"
},
{
"key": "notes",
"label": "Notes",
"type": "string"
},
{
"key": "source",
"label": "Source",
"type": "string"
},
{
"key": "userGroup",
"label": "User Group",
"type": "string"
},
{
"key": "userId",
"label": "User Id",
"type": "string"
},
{
"key": "subscribed",
"label": "Subscribed",
"type": "boolean"
},
{
"key": "createdAt",
"label": "Created At",
"type": "date"
},
{
"key": "favoriteColor",
"label": "Favorite Color",
"type": "string"
},
{
"key": "plan",
"label": "Plan",
"type": "string"
}
]
```
```json Custom-only Response theme={"dark"}
[
{
"key": "favoriteColor",
"label": "Favorite Color",
"type": "string"
},
{
"key": "plan",
"label": "Plan",
"type": "string"
}
]
```
# List mailing lists
Source: https://loops.so/docs/api-reference/list-mailing-lists
GET /v1/lists
Retrieve a list of your account's mailing lists.
## Request
No parameters.
## Response
This endpoint will return a list of mailing list objects.
If your account has no mailing lists, an empty list will be returned.
The ID of the list.
The name of the list.
The list's description. Will be `null` if no description has been added to the list.
Whether the list is public (`true`) or private (`false`). [Read more](/docs/contacts/mailing-lists#list-types)
```json Response theme={"dark"}
[
{
"id": "clxf1nxlb000t0ml79ajwcsj0",
"name": "Mailing List Beta",
"description": null,
"isPublic": true
},
{
"id": "clxf2q43u00010mlh12q9ggx1",
"name": "Product B Launch",
"description": "Get pre-launch updates about Product B.",
"isPublic": true
}
]
```
# List themes
Source: https://loops.so/docs/api-reference/list-themes
GET /v1/themes
Retrieve a paginated list of email themes.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Query parameters
How many results to return in each request. Must be between 10 and 50.
A cursor to return a specific page of results. Cursors can be found from the
`pagination.nextCursor` value in each response.
## Response
### Success
Total results found.
The number of results returned in this response.
The maximum number of results requested.
Total number of pages.
The next cursor (for retrieving the next page of results using the `cursor`
parameter), or `null` if there are no further pages.
The URL of the next page of results, or `null` if there are no further
pages.
The theme ID.
The theme name.
The theme's style attributes. Attributes use the same names as the LMX [``](/docs/creating-emails/lmx#document-styles) tag attributes.
Whether this theme is the team's default theme.
ISO 8601 timestamp for when the theme was created.
ISO 8601 timestamp for when the theme was last updated.
### Error
A `400 Bad Request` is returned if `perPage` is invalid.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
```json Response theme={"dark"}
{
"success": true,
"pagination": {
"totalResults": 1,
"returnedResults": 1,
"perPage": 20,
"totalPages": 1,
"nextCursor": null,
"nextPage": null
},
"data": [
{
"themeId": "thm_01hxyz",
"name": "Marketing default",
"styles": {
"backgroundColor": "#ffffff",
"textBaseColor": "#111111",
"textBaseFontSize": 16
},
"isDefault": true,
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z"
}
]
}
```
# List transactional emails
Source: https://loops.so/docs/api-reference/list-transactional-emails
GET /v1/transactional-emails
Retrieve a list of transactionals emails.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Query parameters
How many results to return in each request. Must be between 10 and 50.
A cursor to return a specific page of results. Cursors can be found from the
`pagination.nextCursor` value in each response.
## Response
### Success
Total results found.
The number of results returned in this response.
The maximum number of results requested.
Total number of pages.
The next cursor (for retrieving the next page of results using the `cursor`
parameter), or `null` if there are no further pages.
The URL of the next page of results, or `null` if there are no further
pages.
The transactional email ID.
The transactional email name.
The ID of the draft email message, if one exists.
The ID of the published email message, if one exists.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
If `perPage` is invalid, a `400 Bad Request` is returned.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"pagination": {
"totalResults": 23,
"returnedResults": 20,
"perPage": 20,
"totalPages": 2,
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
"nextPage": "https://app.loops.so/api/v1/transactional-emails?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
},
"data": [
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email",
"draftEmailMessageId": "cmn5zia4i0017tzli8ric8giv",
"publishedEmailMessageId": null,
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:00:00.000Z",
"dataVariables": [
"firstName",
"lastName"
]
},
...
]
}
```
```json Error response theme={"dark"}
{
"message": "Invalid perPage value"
}
```
# List transactional emails
Source: https://loops.so/docs/api-reference/list-transactional-emails-v1
GET /v1/transactional
Retrieve a list of your transactional emails.
This endpoint is deprecated. Use [List transactional emails](/docs/api-reference/list-transactional-emails) instead.
## Request
### Query parameters
How many results to return in each request. Must be between 10 and 50.
A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.
## Response
This endpoint will return a list of all *published* transactional emails in your account.
Total results found.
The number of results returned in this response.
The maximum number of results requested.
Total number of pages.
The next cursor (for retrieving the next page of results using the `cursor`
parameter), or `null` if there are no further pages.
The URL of the next page of results, or `null` if there are no further
pages.
The transactional email's ID.
The transactional email's name.
The date the email was last updated in [ECMA-262 date-time](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format) format.
Data variables in the transactional email.
```json Response theme={"dark"}
{
"pagination": {
"totalResults": 23,
"returnedResults": 20,
"perPage": 20,
"totalPages": 2,
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
"nextPage": "https://app.loops.so/api/v1/transactional?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
},
"data": [
{
"id": "clfn0k1yg001imo0fdeqg30i8",
"name": "Welcome email",
"lastUpdated": "2023-11-06T17:48:07.249Z",
"dataVariables": []
},
{
"id": "cll42l54f20i1la0lfooe3z12",
"name": "Sign up confirmation",
"lastUpdated": "2025-02-02T02:56:28.845Z",
"dataVariables": [
"confirmationUrl"
]
},
{
"id": "clw6rbuwp01rmeiyndm80155l",
"name": "Invitation",
"lastUpdated": "2024-05-14T19:02:52.000Z",
"dataVariables": [
"firstName",
"lastName",
"inviteLink"
]
},
...
]
}
```
# Publish a transactional email
Source: https://loops.so/docs/api-reference/publish-transactional-email
POST /v1/transactional-emails/{transactionalId}/publish
Publish a transactional's current draft email message.
Content API endpoints are currently in an open alpha and are subject to
change.
The transactional email's current draft email message is published. The draft becomes
the published version and the draft is cleared.
After publishing, use the returned `id` as `transactionalId` with
[Send a transactional email](/docs/api-reference/send-transactional-email) to send
the email.
## Request
### Path parameters
The ID of the transactional email.
### Body
No request body.
## Response
### Success
The transactional email ID.
The transactional email's name.
The ID of the draft email message. This is `null` after a successful publish.
The ID of the published email message.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
If `transactionalId` is invalid, a `400 Bad Request` is returned.
A `404 Not Found` is returned if the transactional email does not exist.
A `409 Conflict` is returned if there is no draft to publish.
A `422 Unprocessable Entity` is returned if the draft failed validation, the
sending domain is not verified, or content was flagged as unsafe.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email",
"draftEmailMessageId": null,
"publishedEmailMessageId": "cmn5bia4i0217tzli8ric8giv",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:30:00.000Z",
"dataVariables": [
"firstName",
"lastName"
]
}
```
```json Error response theme={"dark"}
{
"message": "No draft to publish."
}
```
# Remove suppression for a contact
Source: https://loops.so/docs/api-reference/remove-contact-suppression
DELETE /v1/contacts/suppression
Remove suppression for a contact by email address or user ID.
## Request
### Query parameters
Remove suppression by email or user ID. Only one parameter is allowed.
The contact's email address. Make sure it is
[URI-encoded](https://en.wikipedia.org/wiki/Percent-encoding).
The contact's unique user ID.
## Response
### Success
A message confirming suppression was removed.
The number of suppression removals you can request in a rolling 30 day period.
The remaining number of suppression removals left in the current 30 day period.
### Error
A `400 Bad Request` will be returned if there is an invalid request, the contact is not suppressed, or if you reach your removal quota.
If no contact is found, a `404 Not Found` will be returned.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true,
"message": "Email removed from suppression list.",
"removalQuota": {
"limit": 100,
"remaining": 4
}
}
```
```json 400 response theme={"dark"}
{
"success": false,
"message": "This contact is not suppressed."
}
```
```json 404 response theme={"dark"}
{
"success": false,
"message": "This contact was not found."
}
```
# Send event
Source: https://loops.so/docs/api-reference/send-event
POST /v1/events/send
Send events to trigger workflows.
## Request
### Body
Provide either an `email` or `userId` value or both to identify the contact.\
If both are provided, the system will look for a contact with either a
matching `email` or `userId` value. If a contact is found for one of the
values (e.g. `email`), the other value (e.g. `userId`) will be updated. If a
contact is not found, a new contact will be created using both `email` and
`userId` values.
The contact's email address.\
**Required if `userId` is not provided.**
The contact's unique user ID. This must already have been added to your
contact in Loops.\
**Required if `email` is not provided.**
The name of the event.
An object containing event property data for the event. Values can be of type
`string`, `number`, `boolean` or `date`. [Read more](/docs/events/properties)
Manage the contact's mailing list subscriptions.\
Include key-value pairs of mailing list IDs and a `boolean` denoting if the contact
should be added (`true`) or removed (`false`) from the list. [Read
more](/docs/contacts/mailing-lists#add-contacts-to-lists-with-the-api)
```json theme={"dark"}
{
"mailingLists": {
"cm06f5v0e45nf0ml5754o9cix": true,
"cm16k73gq014h0mmj5b6jdi9r": false
}
}
```
### Contact properties
You can also include default and custom [contact properties](/docs/contacts/properties) in your request body, which will update the contact in Loops. These should be added as top-level attributes in the request.
Contact properties can be of type `string`, `number`, `boolean` or `date` ([see allowed date formats](/docs/contacts/properties#dates)).
```json theme={"dark"}
{
"email": "test@example.com",
"eventName": "signup",
"firstName": "Bob", /* Contact property */
"plan": "pro" /* Custom contact property */
}
```
There are a few [reserved names](/docs/contacts/properties#reserved-names) that you cannot use for custom properties.
To empty or reset the value of a contact property, send a `null` value.
### Headers
Optionally send an idempotency key to avoid duplicate requests.\
The value should be a string of up to 100 characters and should be unique for each request. We recommend using V4 UUIDs or some other method with enough guaranteed entropy to avoid collisions during a 24 hour window.\
The endpoint will return a `409 Conflict` response if the idempotency key has been used in the previous 24 hours.
## Response
### Success
### Error
If you send an idempotency key which has already been used in the previous 24 hours, a `409 Conflict` response will be returned.
All other errors will be `400 Bad Request`.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
# Send transactional email
Source: https://loops.so/docs/api-reference/send-transactional-email
POST /v1/transactional
Send a transactional email to a contact.
## Request
### Body
The email address of the recipient.
The ID of the transactional email to send.
If `true`, a contact will be created in your audience using the `email` value
(if a matching contact doesn't already exist).
An object containing data as defined by the data variables added to the
transactional email template. Values can be of type `string` or `number`.\
If you have added [optional data
variables](/docs/transactional#optional-data-variables) to your email, you can
exclude them from the `dataVariables` object or set the value to `""`.\
If you have added an [array data variable](/docs/transactional#array-data-variables) to your email, make sure to include an array matching the data variables you added to your array block.
Please [contact support](https://app.loops.so/settings?page=support) to enable attachments on your account before using them with the API.
An array containing file objects sent along with an email message.
The name of the file, shown in email clients.
The MIME type of the file.
The base64-encoded content of the file.
To set dynamic Subject, From, Reply to, CC, BCC email header fields, add data
variables to those fields in the editor, then include data for each variable
in the API request. Read our [transactional email
guide](/docs/transactional#data-variables-in-email-headers) for more details.
### Headers
Optionally send an idempotency key to avoid duplicate requests. The value
should be a string of up to 100 characters and should be unique for each
request. We recommend using V4 UUIDs or some other method with enough
guaranteed entropy to avoid collisions during a 24 hour window. The endpoint
will return a `409 Conflict` response if the idempotency key has been used in
the previous 24 hours.
## Response
### Success
### Error
If the transactional email is not found, a `404 Not Found` will be returned.
If you send an idempotency key which has already been used in the previous 24 hours, a `409 Conflict` response will be returned.
All other errors will be `400 Bad Request`.
Deprecated fields will be removed in the future so avoid using them in your
code.
```json Response theme={"dark"}
{
"success": true
}
```
```json Typical error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
```json Error response 2 theme={"dark"}
{
"success": false,
"message": "An error message.",
"path": ""
}
```
```json Error response 3 theme={"dark"}
{
"success": false,
"message": "An error message.",
"error": {
"path": "",
"message": "An error message."
},
}
```
```json Error response 4 theme={"dark"}
{
"success": false,
"message": "An error message.",
"error": {
"path": "",
"reason": "An error message."
},
}
```
```json Error response 5 theme={"dark"}
{
"success": false,
"message": "An error message.",
"error": {
"path": "",
"message": "An error message."
},
"transactionalId": ""
}
```
# Update a campaign
Source: https://loops.so/docs/api-reference/update-campaign
POST /v1/campaigns/{campaignId}
Update a draft campaign.
Content API endpoints are currently in an open alpha and are subject to
change.
Campaigns can only be updated while they are in `Draft` status.
## Request
### Path parameters
The ID of the campaign.
### Body
The updated campaign name.
## Response
### Success
The campaign ID.
The updated campaign name.
The campaign status.
ISO 8601 timestamp for when the campaign was created.
ISO 8601 timestamp for when the campaign was last updated.
The associated email message ID.
### Error
If the request body is invalid, a `400 Bad Request` is returned.
A `404 Not Found` is returned if the campaign does not exist.
A `409 Conflict` is returned if the campaign is not in draft status.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"success": true,
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"name": "Spring announcement v2",
"status": "Draft",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:15:00.000Z",
"emailMessageId": "cmn5zia4i0017tzli8ric8giv"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "Campaign is not in draft status."
}
```
# Update contact
Source: https://loops.so/docs/api-reference/update-contact
PUT /v1/contacts/update
Update or create a contact.
Update an existing contact by sending a request containing contact properties.
This endpoint will create a contact if a matching contact does not already exist in your audience.
If you want to update a contact's email address, the contact will first need a
`userId` value. You can then make a request containing the `userId` field
along with an updated email address.
## Request
### Body
Provide either `email` or `userId` to identify the contact you want to update.\
If both are provided, the system will look for a contact with either a
matching `email` or `userId` value. If a contact is found for one of the
values (e.g. `email`), the other value (e.g. `userId`) will be updated. If a
contact is not found, a new contact will be created using both `email` and
`userId` values.
The contact's email address. If there is no contact with this email, one will
be created.\
**Required if `userId` is not provided.**
A unique user ID (for example, from an external application). [Read
more](/docs/contacts/properties#user-id)
**Required if `email` is not provided.**
The contact's first name.
The contact's last name.
A custom source value to replace the default "API". [Read
more](/docs/contacts/properties#source)
Whether the contact will receive campaign and workflow emails. [Read
more](/docs/contacts/properties#subscribed).
If you send `subscribed: true` in your update calls, contacts who have
previously unsubscribed will be re-subscribed. We recommend leaving this
field out of your requests unless you specifically want to unsubscribe or
re-subscribe a contact.
You can use groups to segment users when sending emails. Currently, a contact
can only be in one user group. [Read more](/docs/contacts/properties#user-group)
Manage mailing list subscriptions.\
Include key-value pairs of mailing list IDs and a `boolean` denoting if the contact should be added (`true`) or removed (`false`) from the list. [Read
more](/docs/contacts/mailing-lists#add-contacts-to-lists-with-the-api)
```json theme={"dark"}
"mailingLists": {
"cm06f5v0e45nf0ml5754o9cix": true,
"cm16k73gq014h0mmj5b6jdi9r": false
}
```
### Custom properties
You can also include [custom contact properties](/docs/contacts/properties) in your request body. These should be added as top-level attributes in the request.
Custom properties can be of type `string`, `number`, `boolean` or `date` ([see allowed date formats](/docs/contacts/properties#dates)).
```json theme={"dark"}
{
"email": "test@example.com",
"plan": "pro" /* Custom property */,
"favoriteColor": "Blue" /* Custom property */
}
```
There are a few [reserved names](/docs/contacts/properties#reserved-names) that you
cannot use for custom properties.
To empty or reset the value of a contact property, send a `null` value.
## Response
### Success
The internal ID of the contact.
### Error
Errors will be `400 Bad Request`.
An error message describing the problem with the request.
```json Response theme={"dark"}
{
"success": true,
"id": "id_of_contact"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
# Update an email message
Source: https://loops.so/docs/api-reference/update-email-message
POST /v1/email-messages/{emailMessageId}
Update fields on an email message.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
The campaign or transactional email that owns this email message must be in `Draft` status.
### Path parameters
The ID of the email message.
### Body
The `contentRevisionId` you last fetched, or the `emailMessageContentRevisionId` you received when creating the campaign. Used for optimistic concurrency.
Email subject line.
Email preview text.
Sender display name.
The sender username (without `@` or domain). The team's sending domain is
appended automatically.
Reply-to email. Must be empty or a valid email address.
The email body serialized as [LMX](/docs/creating-emails/lmx). Styles must be embedded in the LMX
`` tag. The LMX payload must not exceed 100KB.
## Response
### Success
The current content revision ID. Use this value as `expectedRevisionId` on
your next update request.
Non-fatal issues raised while compiling the submitted LMX. Only present on update responses when warnings were produced.
Always `"warning"`.
Optional path for the warning location.
### Error
If the request body is invalid, a `400 Bad Request` is returned.
A `404 Not Found` is returned if the email message does not exist.
A `409 Conflict` is returned when the campaign is not in draft status, when
`contentRevisionId` is stale, when content cannot be parsed, or when the email message uses MJML format.
A `413 Payload Too Large` is returned when the LMX payload exceeds the 100KB limit.
A `422 Unprocessable Entity` is returned when LMX compilation fails.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
```json Response theme={"dark"}
{
"success": true,
"emailMessageId": "cmn5zia4i0017tzli8ric8giv",
"campaignId": "cmp8n3q1w7x2m9k4p6r0t5y8zab2cd",
"subject": "Big spring updates",
"previewText": "A quick look at what's new",
"fromName": "Loops",
"fromEmail": "hello@news.example.com",
"replyToEmail": "support@example.com",
"lmx": "Hello world",
"contentRevisionId": "rev_01hyza",
"updatedAt": "2026-03-28T15:30:00.000Z"
}
```
```json Error response theme={"dark"}
{
"success": false,
"message": "An error message."
}
```
# Update a transactional email
Source: https://loops.so/docs/api-reference/update-transactional-email
POST /v1/transactional-emails/{transactionalId}
Update a transactional email.
Content API endpoints are currently in an open alpha and are subject to
change.
## Request
### Path parameters
The ID of the transactional email.
### Body
The updated transactional email's name.
## Response
### Success
The transactional email ID.
The transactional email name.
The ID of the draft email message, if one exists.
The ID of the published email message, if one exists.
ISO 8601 timestamp for when the transactional email was created.
ISO 8601 timestamp for when the transactional email was last updated.
Data variable names used by the published email. Empty for unpublished
transactionals.
### Error
If the request body is invalid, or if `transactionalId` is invalid, a
`400 Bad Request` is returned.
A `404 Not Found` is returned if the transactional email does not exist.
If the API key is invalid (or content API is not enabled for your team), a
`401 Unauthorized` is returned.
An error message describing what went wrong.
```json Response theme={"dark"}
{
"id": "cmnx1d95z001llilji4vapgqs",
"name": "Welcome email v2",
"draftEmailMessageId": "cmn5zia4i0017tzli8ric8giv",
"publishedEmailMessageId": "cmn5bia4i0217tzli8ric8giv",
"createdAt": "2026-03-28T15:00:00.000Z",
"updatedAt": "2026-03-28T15:15:00.000Z",
"dataVariables": [
"firstName",
"lastName"
]
}
```
```json Error response theme={"dark"}
{
"message": "Transactional email not found."
}
```
# CLI Introduction
Source: https://loops.so/docs/cli
The official command-line interface for Loops. Manage contacts, send events, and deliver transactional emails from your terminal.
## Installation
### Homebrew
```bash theme={"dark"}
brew install loops-so/tap/loops
```
### Script for macOS, Linux, Windows via WSL
```bash theme={"dark"}
curl -fsSL https://cli.loops.so | bash
```
To install a specific version or to a custom path, append `-s -- ` to `bash` in the command above. The default installation path is `~/.local/bin`.
### 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 ` — set a stored key as the default
* `loops auth list` — list stored keys and see which is the default
* `--team ` 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
Log in, switch teams, and manage stored API keys.
Create, update, find, and delete contacts.
Send events to trigger automations.
View your mailing lists.
Send and manage transactional emails.
Create and list contact properties.
Create, list, and update draft campaigns.
Get and update draft email messages.
View saved email themes.
View saved email components.
Upload files as email assets.
Set up shell autocompletion.
## Utility commands
### `loops api-key`
Validate your current API key.
```bash theme={"dark"}
loops api-key
```
### `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
```
# 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 [--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 `). 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). |
## `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 [--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
```
# Campaigns
Source: https://loops.so/docs/cli/campaigns
Create, list, and update draft campaigns.
Content API endpoints are currently in an open alpha and are subject to
change.
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"
```
### Flags
| Flag | Short | Description |
| -------- | ----- | ------------------------------------- |
| `--name` | `-n` | Campaign name Required |
## `get`
Get a campaign by ID.
```bash theme={"dark"}
loops campaigns get clx1234abc
```
## `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 clx1234abc --name "April product update"
```
### Flags
| Flag | Short | Description |
| -------- | ----- | ------------------------------------- |
| `--name` | `-n` | Campaign name Required |
# 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 saved email components.
Content API endpoints are currently in an open alpha and are subject to
change.
The `components` command lets you view [components](/docs/creating-emails/components) saved in your Loops account.
## `get`
Get a component by ID.
```bash theme={"dark"}
loops components get clx1234abc
```
## `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 |
# 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`) Required |
| `--type` | Property type (`string`, `number`, `boolean` or `date`) Required |
There are a few [reserved names](/docs/contacts/properties#reserved-names) that you
cannot use for contact properties.
## `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
```
### Flags
| Flag | Description |
| ---------- | --------------------------- |
| `--custom` | Only list custom properties |
# 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 Required |
| `--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
```
The flags for default properties (i.e. `--first-name` or `--subscribed`) take precedence over the `--prop` flag.
## `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
```
The flags for default properties (i.e. `--first-name` or `--subscribed`) take precedence over the `--prop` flag.
## `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.
Content API endpoints are currently in an open alpha and are subject to
change.
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).
## `get`
Get an email message by ID, including its current content and `contentRevisionId`.
```bash theme={"dark"}
loops email-messages get clx1234abc
```
## `update`
Update fields on a draft email message. The owning campaign must be in `Draft` status.
```bash theme={"dark"}
loops email-messages update clx1234abc \
--expected-revision-id rev_01h... \
--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 clx1234abc \
--force \
--lmx-file ./email.lmx
```
### 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 |
| `--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`. |
At least one content flag (`--subject`, `--preview-text`, `--from-name`, `--from-email`, `--reply-to`, `--lmx`, or `--lmx-file`) must be provided.
# 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 [--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 saved email themes.
Content API endpoints are currently in an open alpha and are subject to
change.
The `themes` command lets you view [themes](/docs/creating-emails/styles) saved in your Loops account.
## `get`
Get a theme by ID.
```bash theme={"dark"}
loops themes get clx1234abc
```
## `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 |
# 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 [--flags]
```
Example:
```bash theme={"dark"}
loops transactional send clx1234abc \
--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 clx1234abc \
--email test@example.com \
--var name=Alice \
--var company=Acme
```
```bash theme={"dark"}
loops transactional send clx1234abc \
--email test@example.com \
--json-vars ./vars.json
```
Add attachments with `--attachment`:
```bash theme={"dark"}
loops transactional send clx1234abc \
--email test@example.com \
--attachment ./attachment.pdf
```
### Flags
| Flag | Short | Description |
| ------------------- | ----- | ------------------------------------------------------ |
| `--email` | | Recipient email address Required |
| `--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 |
## `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 [--flags]
```
Example:
```bash theme={"dark"}
loops send clx1234abc \
--email test@example.com \
--var name=Alice
```
# Uploads
Source: https://loops.so/docs/cli/uploads
Upload files as email assets.
Content API endpoints are currently in an open alpha and are subject to
change.
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 `` 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) |
# 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.
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**.
## 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.
## 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.
Unsubscribed contacts do not count toward [your plan's
limits](https://loops.so/pricing).
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.
## 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.
## 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.
Learn how to create and send transactional email with Loops.
## 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**.
## 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 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**.
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:
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**.
# 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.
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.
## 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, 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 didn't open a specific email within one of your workflows
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.
## 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.
You can create segments while sending campaigns or workflows, or directly on the [Audience page](https://app.loops.so/audience).
To create a segment, first filter the audience table and then click **Save 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.
Just like filters, segments are automatically updated when contact data is changed, meaning emails always send to the correct contacts at 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.
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.
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.
## 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).
Both private and public lists are visible within your Loops admin and can be
used for filtering contacts when sending campaigns and workflows.
## Preference Center
The Preference Center allows your contacts to manage their own subscription preferences.
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).
You can brand your unsubscribe page with a company icon even if you do not use
the lists features.
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.
## Create a list
Go to [Settings -> Lists](https://app.loops.so/settings?page=lists).
Click on the **Create a list** button.
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.
Choose between `Private` or `Public` ([see above](#list-types)).
Click **Save changes** to finalize the creation of the list.
## 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.
After saving your changes, the updated list data will be instantly available
to your contacts in their Preference Centers.
## Delete a list
You can delete lists by clicking on a list's `•••` menu icon and selecting **Delete**.
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)
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.
[Create a campaign](https://app.loops.so/campaigns) or edit an existing one.
On the [Audience page](https://app.loops.so/audience), select the list you want to send to.
Users not subscribed to the selected list will not receive the campaign.
Optionally, you can apply additional [filters or segments](/docs/contacts/filters-segments) to further refine your audience.
### Send workflows to a list
You can configure workflows to only send to contacts in a specific list (this applies to all triggers).
[Create a workflow](/docs/workflows) or edit an existing one.
Select the list you want to trigger the workflow.
Start the workflow. Only contacts from the selected list will be entered into the workflow.
### 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.
[Create a workflow](/docs/workflows) or edit an existing one.
Set the workflow trigger to "Contact added to list".
Select the list you want to trigger the workflow.
Start the workflow. When a contact is added to the selected list, the workflow will be triggered.
### Manually add contacts to lists
How to add existing contacts to your different mailing lists within Loops.
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.
#### Individual contacts
Go to your [Audience page](https://app.loops.so/audience).
Click on the contact you want to manage.
In the contact details page, click on **Subscribed** to reveal the mailing
list dropdown.
Toggle each list on or off as needed. Click **Save changes** in the top
right to apply the changes.
#### Bulk contacts
You can easily add any filtered group of contacts to a specific list on the Audience page.
Go to your [Audience page](https://app.loops.so/audience).
Add filters to segment your audience into the group of contacts you'd like
to add 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.
### 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.
### 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.
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.
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).
### 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.
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.
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.
```html HTML example {3} theme={"dark"}
```
```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",
},
});
```
# Contact properties
Source: https://loops.so/docs/contacts/properties
How to add, edit and delete contact properties.
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.
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`).
### 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`
[CSV uploads](/docs/add-users/csv-upload) accept dates in the formats listed above, but do not support timestamps.
### 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**.
Alternatively you can scroll to the end of the Audience table and click the `+` button at the end of the column headers.
A third way is to go 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.
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.
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.
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.
#### 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
# 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.
You should typically only remove suppression if you're confident the address is now deliverable (for example, the mailbox was restored).
## 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
Free Loops accounts can only request suppression removal via our [support channels](https://app.loops.so/settings?page=support).
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.
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.
## Create a component
To create a component, click on an element in your email and then the **Create component** icon in the editor panel.
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.
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.
## 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.
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
You can tell if an element in your email is a component by looking for a
purple outline around the block in the editor.
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.
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.
If you make local edits to a component that you want to revert, click the **Reset component changes** option.
## 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
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.
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.
## 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 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.
# 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.
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
If you would rather upload emails into Loops, you can [import
MJML](/docs/creating-emails/uploading-custom-email) instead of using the editor.
## 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.
You can choose between different headings, style text **bold**, *italic*, underline 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).
You can also use [keyboard shortcuts](#keyboard-shortcuts) and [Markdown formatting](/docs/creating-emails/editor/markdown) to format your text.
## 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.
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
## 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.
You can also add elements by typing `/` in the editor:
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.
When adding dynamic content like contact properties to [personalize emails](/docs/creating-emails/personalizing-emails), you can type "\{" to open the dynamic content menu.
You can also use [keyboard shortcuts](#keyboard-shortcuts) and [Markdown formatting](#markdown-formatting) to add certain elements to your email.
## 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.
## Footer content
We add an automatic footer into campaign and workflow emails, which includes your company name, address and an 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).
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.
## Keyboard shortcuts
The following keyboard shortcuts are available in the editor.
You can also use [Markdown formatting](/docs/creating-emails/editor/markdown) to format your text.
The editor automatically saves your work as you type so there is no need to
save manually.
| 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 |
| 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 |
# 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.
Arrays are currently supported in transactional emails.
## Adding arrays
You can add array blocks to your email by using the button in the editor toolbar or typing `/` and selecting **Array**.
Arrays are assigned a data variable name (default is `items`) which can be changed in the editor panel in the **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.
## 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**.
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**.
## 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.
# 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.
Read about adding dynamic content in emails using the tag syntax.
# 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.
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.
You can use layouts of 2 to 4 columns. Nesting columns within columns is not
yet supported.
## 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.
## 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.
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.
You can also adjust how much space appears between columns by dragging the green bar between the columns.
### 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).
### 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.
## 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.
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.
Paste in an image URL or add 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.
You can also create dynamic URLs by combining text and dynamic content inside the image source field.
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 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.
# Links
Source: https://loops.so/docs/creating-emails/editor/links
Add static and dynamic links to text, images, and buttons in your Loops emails.
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.
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.
## 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).
Links and opens in transactional emails are never tracked in order to improve
deliverability.
If you want to disable click tracking on individual links in your emails, click the icon in the editor panel.
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.
# 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"}
### Column 1 heading
Column *1* text
### Column 2 heading
Column *2* text
```
**Buttons**
```html theme={"dark"}
Subscribe
```
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.
## 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**
Classic Outlook may render extra padding around sections due to its legacy rendering engine.
## 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.
## 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.
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.
## 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.
Learn about using fallback values for your dynamic content.
## Future improvements
* Content checks
* Boosted spellcheck
* Adhering to design/brand guidelines
Should we add something else? [Let our support team know](https://app.loops.so/settings?page=support).
# LMX
Source: https://loops.so/docs/creating-emails/lmx
Use Loops Markup Language for email content
LMX and related Content API endpoints are currently in an open alpha and are subject to change.
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 emails programmatically.
LMX sits between the API and our editor, making it possible to edit emails across both the editor and API.
## LMX examples
Here are some examples of how to use LMX tags to create emails.
```xml theme={"dark"}
Hello
World
```
**A list**
```xml theme={"dark"}
Release checklist
Finish QAPrepare announcementNotify teams
```
**Two-column layout**
```xml theme={"dark"}
Starter
Best for new projects.
Pro
Advanced automation and analytics.
```
**Full email example**
```xml theme={"dark"}
Hi {contact.firstName}, your weekly update is ready
Here is your weekly account summary. Read the docs for the full changelog.
Top priorities
Fix onboarding drop-off on step 2Publish changelog for API v2Review deliverability metrics
Docs
Start with the quickstart.Review your latest activity and update your settings.
Need help? Contact support.
```
## Core rules
* LMX is XML, not HTML or Markdown. Tags are case-sensitive (for example, ``, not ``).
* LMX is not MJML. Do not use MJML tags (such as ``) or MJML dynamic tag syntax in LMX documents.
* A document is a sequence of top-level **block tags**, optionally with one top-level ``.
* Text is not allowed at the top level. Wrap top-level text in a block tag like ``.
* Top-level inline tags and variables are invalid.
* Required attributes must be present (for example, `` requires `src`).
* Self-closing tags must end with `/>` (for example, ``, ` `).
* Unknown tags are rejected. Unknown attributes are allowed but reported as warnings.
* Attribute values are quoted strings. Numbers and booleans are passed as strings.
* Whitespace between block tags is ignored, so pretty-printing is safe.
* Escape text and attributes as needed (`<`, `&`, `"`).
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.
## Block tags
Block tags define the structure of an email.
| Tag | Description | Self-closing |
| ---------------------- | ------------------------------- | :---------------------------: |
| `` | Style metadata (top-level only) | yes |
| `