# 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. Team switcher 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**. Invitation popover 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. Audience page showing the Import contacts button 2. Hover on **CSV** and click **Upload CSV**. Download the [example formatted CSV](https://app.loops.so/loops_sample.csv) to get an idea of the columns we use. By default, we recommend using at least `Email`, `First Name`, `Last Name`, `User Group` and `Source` columns. Import contacts menu showing CSV upload option 3. After uploading the CSV you'll have a chance to review any duplicates or missing information before finishing. 4. When the upload is finished, all the uploaded contacts can be viewed in the Audience page. That's it! 🎊 ### Some important notes * All new contacts uploaded will be automatically marked as subscribed. If you want to mark imported contacts as unsubscribed add a `Subscribed` column into your CSV file and use "false" as the column value. * The default `Source` value for each uploaded contact will be "CSV Upload". You can change this by adding a `Source` column to your CSV file and specifying a custom value. * Dates in your CSV file must be in one of the accepted formats listed on the [contact properties](/docs/contacts/properties#dates) page (timestamps are not supported). ## Update contacts via CSV You can also upload a CSV file to update your existing contacts in bulk. You can either download a CSV from your Audience page in Loops, edit the data and re-upload it, or start with a new CSV file and just include the contacts and columns you want to update. When updating contacts, Loops will first look for a matching contact using the value in the `User ID` column, then the `Email` column. If a contact is found, Loops will update the contact using the data provided in the CSV file. If a contact is not found with either `User ID` or `Email` values, a new contact will be created using the data provided. ### Some important notes * The importer does not re-subscribe contacts who are marked as unsubscribed in your Audience, so updating the `Subscribed` column to `true` will not re-subscribe the contact. * Cells that are empty in the CSV file will not overwrite existing data in Loops. If you want to clear a field, you will need to provide a value of `null` in the CSV file (4 characters, all lowercase). Note, this only works for `string` type fields. ## Trigger workflows via CSV You can use CSV uploads as a way to trigger workflows on a group of new or existing contacts. If you want to trigger workflows for existing contacts, download a CSV from your [Audience page](https://app.loops.so/audience) then re-upload the file. Only include the contacts you want to trigger workflows for in the CSV file. After uploading the CSV, on the Review page select the **Trigger workflows** option. This will trigger all applicable workflows you have set up with **Contact created** and **Contact updated** triggers. If you select to add contacts to mailing lists on this same page as well as toggle **Trigger workflows** ON, you will also trigger **Contact added to list** workflows. CSV upload review step showing “Trigger workflows” ## View previous CSV uploads After uploading CSV files you can view a history of your uploads plus details for each file's rows. On the [Audience page](https://app.loops.so/audience) click **Import contacts**, hover on **CSV** and click **Upload CSV**. On the next page click on **View imports**, which will show you a list of all of your past uploads. Clicking on one of your uploads will let you view lists of all new, updated or duplicated contacts plus a list of any errors from the import. CSV imports history page in Loops # Add contacts via integrations Source: https://loops.so/docs/add-users/integrations Connect Loops to external platforms to automatically add contacts to your audience. We have a range of integrations with other platforms, which allow syncing of data between services you use. For example, you can add users from thousands of apps through [Segment](/docs/integrations/segment) or [Zapier](/docs/integrations/zapier), or use forms on platforms like [Framer](/docs/integrations/framer) or [Webflow](/docs/integrations/webflow) to let contacts sign up to your mailing list. [Go to the Integrations page](/docs/integrations) to view the full list. # Add and update contacts with the Loops API Source: https://loops.so/docs/add-users/loops-api Loops provides a REST API to manage your contacts. With [the Loops API](/docs/api-reference/intro), you can easily manage contacts directly from your application or service. For example, creating a new contact is as easy as sending a `POST` request to `https://app.loops.so/api/v1/contacts/create`. ```json theme={"dark"} { "email": "test@example.com", "firstName": "Adam", "lastName": "Kaczmarek", "favoriteColor": "blue", "userGroup": "Founders", "source": "Signup form Service" } ``` We also offer endpoints for finding, updating and deleting contacts (plus some other features like sending transactional email and sending events). ## Learn more 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 ` ``` ## Read more # Set up a welcome email sequence for new Ghost members Source: https://loops.so/docs/guides/ghost-email-sequence Send automated email sequences to new Ghost subscribers using Zapier and Loops. This guide helps you set up email sequences that get sent to all new subscribers to your Ghost site. With Loops, you can set up sophisticated sequences called [workflows](/docs/workflows), allowing you to send a range of welcome emails to your members over a period of time. Using [Zapier](https://zapier.com), we can automatically add every new member who signs up on your Ghost site to your Loops audience. ## Set up the Zapier Trigger The first step is to connect your Ghost site to Loops using Zapier. Sign up to Zapier and create a new Zap using Ghost's **Member Created** Trigger. Add Ghost trigger This creates an automation that will trigger every time a new member is created in Ghost. Zapier will then send the contact's information over to Loops. Now connect Ghost from the Trigger by clicking **Account** then **Sign in** and then pasting in your Ghost API key and API URL. To find these, go to Settings in your Ghost admin, search for "Integrations" and click on **Zapier**. Sign in to Ghost ## Set up the Zapier Action Next you need to link up Loops as the Action. Click the **Action** node, search for Loops and select the **Send Event** option in the Event dropdown. Click **Continue**, then **Sign in** and paste in your Loops API key (which you can find from your [API Settings page](https://app.loops.so/settings?page=api)). Sign in to Loops Instead of using an event for the workflow trigger, you could also choose to send the workflow to every new contact created in your audience. We choose the event trigger in this example as you may add other contacts to your audience from other sources than Ghost. Now click **Continue** to move to the **Action** tab. This is where you can configure which Ghost data is sent to Loops. As you click each field, you can select the different Ghost-provided data. In **Email**, select the Email field. In **User ID**, select the ID field. In the **Event Name** field, write something like `newMember`. This is the name we'll use in Loops to trigger the email sequence. You can use any name, but make it descriptive. You'll need this name in the next step inside Loops. Adding an event name If you want to include contact-related information in your emails you can use [event properties](/docs/events/properties). To do this, add more Ghost data in the **Event Properties** field. For example, if you want to include the user's name, subscribed status, member status or your newsletter's name, you can add these properties here. Click the `+` button to add new properties each time. Adding event properties Click **Continue** to see an overview of the event and its data that you'll send to Loops. Here you can test the action works by clicking **Test Step**. This will send actual data to Loops, which you will see in the Event Log section on the [Events page](https://app.loops.so/settings?page=events). When you're happy everything is set up properly, click **Publish**. If you ever change the event properties sent from Zapier, you need to update the event data in Loops to match. You can do this from [Settings -> Events](https://app.loops.so/settings?page=events). Click on the event and edit the listed properties. [More info](/docs/events/properties#editing-event-properties) ## Create an email sequence Now that the connections are set up, you can create the email workflow in Loops. Go to Loops and click on **Workflows** in the sidebar. Click **New**, which will create a new workflow and show the [workflow builder](/docs/workflows). Select the **Event is fired** trigger option. Click on the **Event received** trigger in the workflow builder and enter the name you entered in Zapier in the previous step (in this example, `newMember`). Select the event trigger You can edit the **Timer** node if you want to add a delay between the event being received by Loops and the email being sent to your Ghost members. Set a timer delay You can also add a filter to the audience for this workflow, if you want to limit sending to a certain sub-group of your members. If you want the workflow to send to all new Ghost members, just leave it as "All contacts". Lastly, click the **Send email** node and then **Create email**. You'll see the [email editor](/docs/creating-emails/editor) open, where you can create your email. If you opted to send event properties from Zapier, you can add them to your email by clicking the `⚡️` button above the editor (1) and then configuring in the **Event Property** editor panel (2). Adding event properties in the editor Once your first email is complete, you can add more Timer and Email nodes to your workflow to complete your email sequence. Just click on the `+` icon between nodes to add new ones. ## Learn more Read more about triggering emails with events. Learn how to create stylized emails and add personalization. } href="/integrations/zapier" > Manage contacts and send emails from thousands of other platforms. # Why we don't support HTML emails Source: https://loops.so/docs/guides/html-emails Understanding why Loops doesn't support HTML email content and the benefits of our approach. This guide explains why Loops doesn't support HTML email content in your requests, and how this approach benefits your team in the long run. We'll cover the common questions we receive and explain our philosophy on email management. ## The multi-provider problem In our experience working with hundreds of software companies, we've noticed a pattern: by the Series B stage, most companies are using 3-6 different email sending providers. This fragmentation happens because different teams have different needs: * Growth teams need advanced automation tools * Marketing teams focus on promotional content * Product teams send surveys and feature-specific emails * Sales teams need targeted outreach * Customer success teams handle support emails * Engineering teams want simple, maintainable solutions Each department gets its own tool, requiring coordination between design, marketing, legal, and engineering to keep everything working together. **We believe there's a better way.** ## Why we don't support HTML content We intentionally don't support HTML content in your requests because we don't believe emails belong in your codebase. Here's why: ### Branding management When you need to update your brand colors, footer copy, or logo, you shouldn't need to: 1. Open a pull request 2. Update code 3. Deploy changes 4. Test across all email types In Loops, these changes are a single click away and apply instantly across all your emails, whether they're transactional, marketing, or product updates. Even better, non-technical team members can make these changes without involving engineering. ### Email client compatibility Email clients are notoriously inconsistent in how they render HTML. We maintain daily updates to our email rendering system to handle things like: * New email client versions * Dark mode changes * Mobile rendering quirks * Legacy client support By handling this complexity for you, we ensure your emails look great everywhere, from the latest iPhone to that ancient Outlook version your biggest client refuses to update. ### Long-term support Email clients evolve slowly but constantly. Keeping up with that is literally our full-time job. By taking HTML out of your codebase, we're basically giving you a free email-compatibility team that monitors email client changes, updates, tests across all major clients, and handles edge cases. ### Security and deliverability When major email providers like Gmail and Yahoo update their guidelines, we catch changes early and update our system proactively, ensuring your emails remain compliant and maintain high deliverability rates. ### Team collaboration Keeping your emails out of your codebase means any member of your team can make changes without engineering involvement. It's also possible to preview new content or designs without affecting live emails, and make changes instantly without deployment. ## Summary You probably have better things to do than manage email templates. Our approach means: * Your team can update emails without engineering * Changes are instant and safe * Emails look great everywhere * You maintain consistent branding * We handle all the technical complexity ## Read more # Guides for onboarding, deliverability, and lifecycle emails. Source: https://loops.so/docs/guides/intro Browse Loops guides on onboarding, deliverability, lifecycle emails, integrations, and more. ## Getting started Start sending email with the best results. ## Deliverability Improve your email deliverability with these guides. ## Integrations How-to guides for connecting Loops to other platforms. } href="/guides/better-auth-emails" /> } href="/guides/bubble-api-connector" /> } href="/sdks/javascript/nextjs" /> ## Creating emails How to create different kinds of emails in Loops. ## Email basics Learn more about email. ## Docs blog * [How we create our documentation](/docs/guides/how-we-work-documentation) # Recipe: SaaS customer lifecycle emails Source: https://loops.so/docs/guides/lifecycle-emails How to send onboarding, dunning and churn emails to your customers with workflows. For SaaS companies, there are a few important events in a customer lifecycle that email can help with. * **Acquisition emails** are emails sent after a user signs up for a waitlist, platform or newsletter. * **Onboarding emails** help brand new users get familiar with—and get the most out of—your product. * **Retention emails** keep your users engaged long-term. * **Re-engagement emails** attempt to get users active again if they haven't used your platform recently. * **Dunning emails** help reduce churn by prompting the user to re-activate a subscription or fix payment issues. * **Re-activation emails** attempt to bring users back after a cancellation. With Loops, you can easily set up workflows plus our API (or an integration) to help with each of these use cases. ## How it works To get this set up in Loops, the idea is to create a [workflow](/docs/workflows) for each of the types of email you want to send. So there would be a workflow for activation emails and a workflow for dunning emails and so on. A workflow is like an email sequence containing emails, time delays, audience filters and an initial trigger. We will use a [custom contact property](/docs/contacts/properties) called `subscriptionStatus` to enter users into each of the workflows at different times in their subscriptions. This property is used as the [workflow trigger](/docs/workflows/triggers); if the property is ever changed using [an integration](/docs/integrations) or the [Loops API](/docs/api-reference/intro), we trigger a workflow, which will send emails to the contact. ## Add a contact property First, let's add the contact property to the audience in Loops. Go to your [Audience page](https://app.loops.so/audience). Click on a table header to show the dropdown then select **Add property**. Enter "Subscription status" into the **Name** field and make sure **String** is selected in the "Type" field. (You'll notice that the "stored name" of your property is `subscriptionStatus`. This is the name we'll use in the API and integrations). Click **Add Property** when you're done. Your new property was added to the far right of the Audience table. ## Update contacts Now you can click on contacts and update the value manually one-by-one, or use the API or an integration to update contacts programmatically. To update contacts in bulk you can download your Audience as a CSV, update values and [re-upload the file](/docs/add-users/csv-upload). ## Create workflows The next step is to create the workflows, one for each type of email. Repeat the following step for each of the different subscription statuses you want to send emails for. Go to the [Workflows page](https://app.loops.so/loops) in your account and click **New**. Select the **Contact update** option, which will create a new workflow using that trigger. Select trigger You will enter the [workflow builder](/docs/workflows). Click on the **Contact updated** node to set up the workflow trigger. ### Set up the trigger 1. Select the "Subscription Status" property from the **When** dropdown. 2. The **Changes from** dropdown should have "Any value" selected. 3. Next you need to specify what value Loops should look out for to enter users into the workflow. For example, for onboarding emails, you can choose "is empty" from the **To** dropdown. For re-activation emails select "Equals" from the **To** dropdown and enter a relevant value like "Canceled". 4. **Trigger time** should be set to "Every time"; this will make sure users are entered into the workflow every time that the Subscription Status value matches. ### Create emails Now create the email(s) you want to send from each workflow. You can add as many emails as you like into each workflow, separated with timers. If you want to send emails to certain groups of contacts, you can use Audience filters in your workflow. We have a range of [email templates](https://app.loops.so/templates) available, which you can use as a base for all of your subscription emails. Templates If you want to use cohesive branding in your emails, make use of [themes](/docs/creating-emails/editor#themes) in the editor. Editor panel ## Update the Subscription status value The final step is to make sure that your contacts have the correct "Subscription status" value assigned to them in Loops. This will make sure they are in the correct workflows at the correct moment within their subscription. Using the API you can make a simple `PUT` call to the [Update contact endpoint](/docs/api-reference/update-contact), with a request containing the contact's email address and subscription status. (You can also add more contact properties in this call if you want to add more data to the contact in Loops.) Using the Update contact endpoint will either create or update a contact in Loops using the provided email address. ```json theme={"dark"} { "email": "test@example.com", "subscriptionStatus": "Canceled" } ``` Many of [our integrations](/docs/integrations) allow syncing of contact data. Just make sure you create or update contacts and include the current `subscriptionStatus` value. Now you have set up different workflows to trigger when the "Subscription status" value changes, your users will be automatically entered into each workflow and receive the correct emails at the correct times during their subscription lifecycle. ## Example workflows Here are some example workflows you can create along with a suggested trigger and workflow contents. ### Onboarding Sent to brand new users. * **Trigger**: Contact added or Contact updated (e.g. `subscriptionStatus` is empty). * **Audience filter**: Match the trigger (e.g. `subscriptionStatus` is empty). * **Workflow contents**: 1–5 welcome and onboarding emails over the first 30 days. ### New subscribers Sent to users who just started paying. * **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Paying"). * **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Paying"). * **Workflow contents**: 1–3 emails about paid-only features over the next 3 days. ### Dunning Sent to customers who had a first failed payment. * **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Failed"). * **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Failed"). * **Workflow contents**: 1–3 emails during your dunning period. ### Churn Sent to customers who have just canceled their payments. * **Trigger**: Contact updated (e.g. `subscriptionStatus` changes to "Canceled") * **Audience filter**: Match the trigger (e.g. `subscriptionStatus` equals "Canceled"). * **Workflow contents**: 1 email saying goodbye and asking for feedback. # Set up Loops in Next.js Source: https://loops.so/docs/guides/nextjs How to send email from your Next.js project with Loops. This guide shows how to add Loops to your Next.js project, so you can send transactional emails, manage contacts and trigger automated emails. ## Install the SDK The first step is to install the [Loops SDK](/docs/sdks/javascript). This is written in TypeScript so you can benefit from strict types when coding. ```bash theme={"dark"} npm i loops ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You'll need an API key to use the SDK. Go to your [API Settings page](https://app.loops.so/settings?page=api) in Loops to generate and copy a key. Save this value in your environment variables as something like `LOOPS_API_KEY`. Then you can import the Loops SDK client like this: ```javascript theme={"dark"} import { LoopsClient } from "loops"; const loops = new LoopsClient(process.env.LOOPS_API_KEY); ``` You can also use the [Loops API](/docs/api-reference/intro) directly in your app, without the SDK. [Read more](#using-the-api-instead) Explore our official JS/TS SDK. Read the Loops API reference. ## Server-side only It is important that you only use the Loops API and SDK from server-side code. If you make calls directly in the browser, you risk exposing your API key, which would give other people read and write access to your Loops account data. Additionally, the Loops API does not support cross-origin requests made from client-side JavaScript. If you want to make calls from the browser—for example, to collect newsletter subscriptions from a form—create proxy endpoints. To add a new contact, create an internal API endpoint and use the Loops API/SDK within it. ```typescript app/api/contacts/route.ts theme={"dark"} import { NextRequest, NextResponse } from "next/server"; import { LoopsClient } from "loops"; const loops = new LoopsClient(process.env.LOOPS_API_KEY as string); export async function POST(request: NextRequest) { const res = await request.json(); const email = res["email"]; // Note: updateContact() will create or update a contact const resp: { success: boolean; id?: string; message?: string; } = await loops.updateContact(email); return NextResponse.json({ success: resp.success }); } ``` ## Send transactional email A big use case for using Loops in a Next.js project is to send transactional email to users. These emails are one-off emails, which help users with your product, for example password reset emails notification emails. To create a transactional email, go to the Transactional page in Loops. Click **Create** or select a template. Create the email in [the editor](/docs/creating-emails/editor), which gives you rich formatting options and components. Creating a transactional email To add dynamic content (like rest password URLs or user data) you can add [data variables](/docs/transactional#add-data-variables) into the email from the toolbar. Give each data variable a unique name. You can populate these variables from your code when sending the email via the SDK in the next step. Make sure to Publish your transactional email when you're done. Now your email is created you can start sending emails. In your code, call `sendTransactionalEmail()` and include values for each of the data variables you added to your email. ```javascript JavaScript theme={"dark"} const dataVariables = { loginUrl: "https://myapp.com/login/", }; const resp = await loops.sendTransactionalEmail({ transactionalId: "transaction_email_id", email: "test@example.com", dataVariables }); if (!resp.success) { // The sending failed } else { // The email was sent successfully } ``` ```typescript TypeScript theme={"dark"} const dataVariables: { loginUrl: string } = { loginUrl: "https://myapp.com/login/", }; const resp: { success: boolean, path?: string, message?: string } | { success: false; error: { path: string; message: string; }; transactionalId?: string; } = await loops.sendTransactionalEmail({ transactionalId: "transaction_email_id", email: "test@example.com", dataVariables }); if (!resp.success) { // The sending failed } else { // The email was sent successfully } ``` The response will contain a `success` boolean telling you if the email was sent successfully. If it was not, you'll also receive an error message. Read more in the SDK docs. ## Sync users to Loops Another main use case for teams using Loops is to keep their Loops audience updated when user data changes in their application. To do this you can use the `updateContact()` method. `updateContact()` can be used as a shortcut "update or create" function. It will create new contacts if the provided email address and/or user ID are not found. For example, you may store custom data in Loops like subscription plan level or user usage information that you include in emails. You can update contacts in Loops like this: ```javascript JavaScript theme={"dark"} const contactProperties = { userId: 826, planName: "Pro" /* Custom property */, usage: 172629 /* Custom property */, }; const resp = await loops.updateContact("test@example.com", contactProperties); if (!resp.success) { // The call failed } else { // The contact was updated OK } ``` ```typescript TypeScript theme={"dark"} const contactProperties: Record = { userId: 826, planName: "Pro" /* Custom property */, usage: 172629 /* Custom property */, }; const resp: { success: boolean, id?: string, message?: string } = await loops.updateContact("test@example.com", contactProperties); if (!resp.success) { // The call failed } else { // The contact was updated OK } ``` The TypeScript example above shows how to properly type your `contactProperties` object and the expected response from the `updateContact` method. We recommend always populating the `userId` value for users, which should be their unique value in your platform. This allows you to change a contact's email address in the future, because they have a separate unique identifier in the system. Read more in the SDK docs. ## Trigger workflows with events A third example of using the Loops SDK is to trigger [workflows](/docs/workflows). Workflows are automated email sequences, which can send multiple emails to contacts. You can trigger these emails using [events](/docs/events), and you can send events to Loops using the SDK. For example, you may have a workflow that you send to new users after they have completed an onboarding flow in your app. First, [create a new workflow](https://app.loops.so/loops) using the "Event received" trigger. Add emails, timers and audience filters to your workflow as you wish. Then to trigger this email sequence, send an event to Loops. If your event name is `completedOnboarding`, your call would look like this... ```javascript JavaScript theme={"dark"} const resp = await loops.sendEvent({ email: "test@example.com", eventName: "completedOnboarding", }); if (!resp.success) { // The event was not sent } else { // The event was sent OK } ``` ```typescript TypeScript theme={"dark"} const resp: { success: boolean, message?: string, } = await loops.sendEvent({ email: "test@example.com", eventName: "completedOnboarding", }); if (!resp.success) { // The event was not sent } else { // The event was sent OK } ``` Read more in the SDK docs. ## Using the API instead If you prefer, you can use the [Loops API](/docs/api-reference/intro) directly instead of using the SDK. You should never call the API from your front-end code as this will expose your API key. For example, you can send a transactional email like this: ```javascript theme={"dark"} const data = { email: "test@example.com", transactionalId: "abcdefg", dataVariables: { loginUrl: "https://myapp.com/login/?code=1234", }, }; return fetch("https://app.loops.so/api/v1/transactional", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.LOOPS_API_KEY}`, }, body: JSON.stringify(data), }) .then((response) => response.json()) .then((response) => { if (!response.success) { // The sending failed } else { // The email was sent successfully } }) .catch((err) => console.error(err)); ``` On Vercel, each backend function gets its own lambda. Make sure you use `return` otherwise the lambda might be terminated before the promise is evaluated. Read through our API documentation. # Recipe: Onboarding completion workflow Source: https://loops.so/docs/guides/onboarding-completion Send a congratulatory email the moment a user finishes your onboarding checklist, plus a next-step nudge if they don't. A common SaaS lifecycle pattern: celebrate when a user completes onboarding, and re-engage the users who stall. This recipe sets up both with two [workflows](/docs/workflows). The core idea: fire an [event](/docs/events) from your app when the user completes onboarding and update an `onboardingCompletedAt` contact property in the same request. The celebration workflow triggers from the event, and the stalled-user workflow filters out contacts with a completion timestamp. ## What you need * A timestamp [contact property](/docs/contacts/properties) like `onboardingCompletedAt` * A named [event](/docs/events) for "onboarding completed" * The [Loops API](/docs/api-reference/intro) or an [integration](/docs/integrations) writing into Loops ## Set up the trigger Use the **Event received** [workflow trigger](/docs/workflows/triggers) with an event name like `onboardingCompleted`. Update the contact's `onboardingCompletedAt` property at the same time so your stalled-user workflow can exclude users who already finished. If you prefer using contact property changes as the trigger, see the **Contact updated** approach used in [the lifecycle emails guide](/docs/guides/lifecycle-emails). Call the [Send event endpoint](/docs/api-reference/send-event) when the user finishes the last onboarding step. The API accepts contact properties as top-level fields in the same request: ```json API request theme={"dark"} { "email": "user@example.com", "onboardingCompletedAt": "2026-04-27T17:00:00Z", "eventName": "onboardingCompleted", "eventProperties": { "daysToComplete": 2 } } ``` ```ts SDK theme={"dark"} await loops.sendEvent({ email: "user@example.com", eventName: "onboardingCompleted", contactProperties: { onboardingCompletedAt: new Date().toISOString(), }, eventProperties: { daysToComplete: 2 }, }); ``` In the [Workflow builder](/docs/workflows), pick **Event received** and select `onboardingCompleted`. Add a **Send email** node. Congratulate them, surface the next high-value feature, and offer a next action. ## Catching users who stall The other half of this recipe is a separate workflow that fires when a contact is added but does not complete onboarding within a timer window. Pick **Contact added** as the [trigger](/docs/workflows/triggers). Wait 3 days. Use an [audience filter](/docs/workflows#audience-filters) to only send if `onboardingCompletedAt` is empty. Keep it short. Link to the specific step they missed if you track that. ## Measuring success * Open rate on the celebration email: should be >50% because it is expected * Click-through on the stall nudge: track with the `email.clicked` [webhook](/docs/webhooks) * Completion lift: compare `daysToComplete` distributions before and after this workflow ships ## Read more # Your first onboarding emails Source: https://loops.so/docs/guides/onboarding-emails Some best practices for building your sender reputation with onboarding emails. We covered the basics of a Sender Reputation in [this guide](/docs/deliverability/sending-reputation) and this guide will cover some best practices for building your sender reputation with onboarding emails. If you're new to Loops, we recommend taking some specific steps to help warm up your sending reputation and to ensure your emails land in inboxes. We want to start with an onboarding workflow and transactional emails before we send larger campaigns to a large list. When we send those larger campaigns, we want to first start with a small list and then slowly increase volume while monitoring the results. ## Onboarding welcome workflow The first step is to set up a welcome email sent to users after they sign up to your application or subscribe to your list. Recipients of these emails are expecting the email and are most likely to engage with it. For example, new users who sign up could expect a welcome email from you, welcoming them to your platform and explaining some initial steps to get them started. Here is an example of the email we at Loops send out to new users: Loops' own welcome email Check out our library of useful templates for creating welcome emails. Users receive these emails because they expect them, so they are more likely to open and engage with them, building your sender reputation. Get started by [creating workflows](/docs/workflows), which are email automations used to send emails after certain triggers, like a new contact joining your audience or after an event happens in your platform. ### Creating a welcome workflow in Loops A simple welcome workflow Go to your [Workflows page](https://app.loops.so/loops) and click **New**, or select from one of our [ready-made templates](https://app.loops.so/templates). Set your trigger to **Contact added** if you want the workflow to fire for every new contact. You can also set a filter to only send to contacts with a specific tag or property, or specifically target it to a specific list. Select **Event received** if you want to trigger the workflow based on something happening in an external platform. Next, write your email by clicking on the **Send email** node.\ Make sure to make your workflow active by clicking **Start**. ## Transactional emails Transactional emails are by definition emails that are sent to users because of an action they took or need to take to use your platform. Since all emails from Loops send from a single domain, we can also use these emails to build our sender reputation. We recommend starting with these essential transactional emails: * Login verification emails * Password reset emails * Account confirmation emails These types of emails have high engagement rates since users actively request them, which helps build your sender reputation. Password reset email example Check out our library of useful transactional email templates. ## Targeted campaign Once your welcome workflow has been active and sending for 2 or 3 days, you should see an open rate of around 40% on the first email after the welcome email. Now you can start to send a campaign to a small list of engaged users. Here are two recommended approaches: 1. Target recent signups using the `createdAt` property to find users added in the last 3 days 2. Target active users by syncing login data to track your most engaged users For your first campaign content, we recommend: ✅ Product updates or new feature announcements ❌ Avoid generic newsletters, giveaways or promotional content Important metrics to monitor: * Campaign open rate should be above 30% * Welcome workflow open rate should stay above 40% * If either drops below these thresholds, pause sending and re-evaluate Loops helps to mitigate deliverability issues by sending campaign emails in batches, waiting to check open rates and then sending further emails in your list depending on the results. We take great care to make sure your emails have the highest deliverability as possible from our side. ### Sending a campaign to active users in Loops You can use the default `createdAt` contact property, or you can create custom [contact properties](/docs/contacts/properties#custom-contact-properties) and use the [API](/docs/api-reference/update-contact) or an [integration](/docs/integrations) to update contacts when an event happens in your account (e.g. a log in). For example, you could use the name `lastActive` and choose the "Date" field type. Then whenever the user does a major action in your application, update the contact with a new `lastActive` value: ```javascript theme={"dark"} POST /v1/contacts/update { "userId": user.id, "lastActive": new Date().getTime() // timestamp in milliseconds } ``` Once you have created your campaign in Loops, you need to specify the audience based on your chosen date field. In the "Audience" tab, choose "Last Active" (or "Created At"), then "After" and then pick the date three days ago from the date field. Selecting an active audience ## Larger campaigns Now you have warmed up your account, you can start to send out larger campaigns to more contacts. You may want to import users from another email platform of your application's user database. We still recommend to not send to everyone on your list. Try to identify segments in your user list (especially if it's in the thousands) that are most likely to open emails from you. ## Read more # Open rates are a vanity metric Source: https://loops.so/docs/guides/open-rates Email open rates are unreliable due to privacy changes like Apple Mail Privacy Protection. Learn what to track instead when measuring engagement. When it comes to email marketing, there are countless metrics that you can track. Some of these metrics may be fun to track and look great on paper (your computer screen) but unfortunately don’t actually lead to improved business results. These are vanity metrics. While you might think that vanity metrics help paint a picture of success or improvements to unknowing stakeholders, they are ultimately a distraction. Facebook likes, Twitter followers, blog pageviews… these are all vanity metrics because they don’t materially impact the success of your business in a tangible way. But so are email open rates. Yes, you read that right. The main metric you’ve been tracking (and sharing) to gauge the success or failure of your email marketing campaigns is (mostly) just for show. ## What is an email open rate? An email open rate is the number of unique opens your email campaign receives. Calculating open rate is simple: Number of unique email opens / Number of delivered emails x 100 = Open Rate Delivered emails are important here because hard bounced emails do not count toward this calculation. Note: a hard bounced email is an email that cannot be delivered due to an unchanging and permanent reason. Unfortunately, there is nothing that you can do to reverse this to force your email through. Some common reasons for a hard bounce are recipient email address doesn’t actually exist, recipient mail server doesn’t exist, and invalid domain name. While a hard bounce will prevent your email from arriving in its intended inbox it is possible that a hard bounce could be caused by something as simple as your recipient having a typo in their email address. To keep things simple, let’s assume you sent and delivered an email to 100 different users in your campaign. 23 of your users opened this email giving you an open rate of 23%. 23 / 100 = .23 x 100 Easy enough, right? Up until recently, open rates were a universally loved way to gauge campaign performance. So what changed? ## Your open rate is (now) a vanity metric Apple changed — mostly. With a dominating [59.8% email client market share](https://www.litmus.com/blog/email-client-market-share-february-2022/), any change they make is a huge deal. New privacy initiatives from many of the largest email clients are quietly removing email senders ability to successfully track open rates. Apple is leading the charge with their new “Mail Privacy Protection” (first introduced in 2021 with iOS 15) that allows users to opt in to having Apple pre-load their email upon receipt. By doing so, the email’s tracking pixel will immediately trigger. Note: an email tracking pixel is a 1px by 1px square image that is inserted into an email and is transparent in color and invisible to the recipient. These tracking pixels are what allow marketers to measure open rate, click rate, traffic sources and more. This change artificially inflates open rates as delivered emails will now automatically show as opened whether they truly have or have not been opened by the intended recipient. These changes to email open tracking are completely out of the sender’s control. And to top it all off, these changes affect not only users receiving emails on the Apple Mail app itself but also those who use an Apple client in any way (r[egardless of email server](https://www.litmus.com/blog/apple-mail-privacy-protection-for-marketers/)). This means that using GMail, Yahoo, Outlook, etc. on iOS 15 may also contribute to your now-inflated open rate. What was once a key metric to track and share with key stakeholders should now be viewed with a bit more skepticism. ## What’s next? It is highly unlikely that these recent privacy changes will be reversed anytime soon (ever). So what can you do about it? The answer may surprise you. We suggest continuing to run your campaigns as you have been. Continue to send consistent emails (both in frequency and content), create intriguing and accurate subject lines and body content, segment your list so that only the most relevant audience is receiving your emails, and pay close attention to your unsubscribe rate. However, now you should put a greater emphasis on tracking specific product goals in relation to your email campaigns. Have actionable metrics in mind – metrics around business goals such as generating free trials, converting trials to paid, increased usage of your app or inviting team members. And actually, this should have been the goal all along. High open rates don’t necessarily lead to increased sales or profits for your business, the end result of those opens is what really makes the difference. Now that your focus has transitioned to more tangible KPI’s that lead to specific product goals being met it will be much easier to accurately measure the success of your email marketing campaigns. Looking back on it, maybe your email open rate becoming a vanity metric overnight will ultimately be a blessing in disguise. Take this as an opportunity to refocus your attention on the KPI’s that ultimately matter for your business. High open rates may have been impressive in the past but now it’s time to craft and measure your email campaigns against the things that truly move the needle. # Recipe: Product updates Source: https://loops.so/docs/guides/product-updates Our updated, definitive guide for sending product updates. ## Introduction A product update should be sent once a month with updates about what you shipped recently. This typically includes new features, improvements, and bug fixes. Things to keep in mind: * Brevity is key. Users don't want to read a novel. * If you send valuable content, users will come to expect (and open) it. * It's okay to send multiple emails in a month if that's your shipping cadence. Learn more tips for crafting effective emails and improving open rates. ## How to craft a product update email: Example of a product update email sent from Loops 1. [Create a new email Campaign](/docs/creating-emails/editor) 2. Add your logo at the top of the email, [save it as a component](/docs/creating-emails/components) if you haven't already. 3. Add a simple subject line, like "The latest updates from Loops" 4. Add a simple intro paragraph, with a link to the full changelog, potentially socials and a highlight of the most important changes along with a call to action to read the full email. 5. Try to limit the number of updates to 2-3, and make sure they are relevant to the content. It's better to have a single or a couple impactful, relevant updates than a long list of updates that are not relevant to the user. 6. In the footer, add a link to the full changelog, socials, and a link to unsubscribe. 7. [Send the email!](/docs/sending-first-email) Be careful not to overwhelm your users with too many updates. Focus on the most impactful changes. ## Choosing your audience ### For new senders If you're just starting out, send to all users and maintain a steady cadence over time. ### For established senders If you have an existing list but haven't sent product updates before: * Send to active users who have engaged with your product in the last 30 days * Include users who have created an account in the last 30 days * If the audience size is less than 5,000 users, send to all users Learn how to create targeted segments for your product updates. Learn how to create and send your first email in Loops. Add dynamic content to make your product updates more relevant. Create reusable elements for consistent product update emails. Explore different types of emails you can send with Loops. # Retrigger a Workflow Source: https://loops.so/docs/guides/retroactively-trigger-workflow Backfill contacts into a workflow. This is a temporary workaround while we build native backfill. The current flow is clunky and will get much better. Please still let us know you want native backfill so we can prioritize it. ## When to use this * You have an existing workflow that should have run for some contacts but didn’t. * You’re comfortable exporting and re-importing a contact list. ## What you’ll do (high level) 1. Duplicate the original workflow. 2. Create a one-off contact property (e.g., `backfill_welcome_sep2025`). 3. Change the duplicate workflow’s trigger to “Contact updated” based on that property. 4. Start the duplicated workflow. 5. Export the intended audience. 6. Add the property to the CSV for those contacts. 7. Re-import the CSV to update contacts and trigger the workflow. ## Step-by-step Open the workflow you want to retroactively trigger. Use **Duplicate** to create a copy. Rename it clearly (e.g., “Welcome Backfill (Sep 2025)”). Add a new contact property to use exclusively for this backfill. **Example:** Boolean: `backfill_welcome_sep2025` (true/false) Use a name that avoids collisions with any existing logic. Select “Contact updated” as the trigger. Set the criteria to: * was any * is equal to `true` For one-off backfills, set Trigger frequency to One time so a contact only enters once. From the main audience table, [export the contacts](/docs/contacts/export-contacts) who should be backfilled. Open the CSV and add a new column with the property key you created. Populate that column with `true` for every row you want to trigger. Ensure the CSV still includes the primary identifier (either `email` or `userId`). Import the CSV from the **Import** button on the [Audience page](https://app.loops.so/audience). Confirm the column mapping so your backfill property maps 1:1 to the new contact property. Set the CSV import to [trigger workflows](/docs/add-users/csv-upload#trigger-workflows-via-csv) using the **Trigger workflows** toggle. Start the import. Your backfilled workflow will begin triggering immediately. After the backfill completes, consider removing or resetting the temporary contact property to avoid accidental future triggers. # Recipe: Scheduled digest email Source: https://loops.so/docs/guides/scheduled-digest-email How to send a daily, weekly or monthly email with a summary of what's happened in your app. This email type may also be referred to as a "rollup" email or a "summary" email. The idea is to send a single email that summarizes what's happened in your app over a period of time. These kinds of digest emails are a great way to keep your users engaged with your app. The best way to do it today is a workflow with an [event trigger](/docs/workflows/triggers) set to fire “every time” with an event payload containing the updated property you’d like to send. Then at the end of the day, week or month you can trigger a digest email with a summary of the events that happened. ## Create the workflow and event Go to the [Workflows](https://app.loops.so/loops) page and create a new workflow. Choose the **Event is fired** trigger. You will enter the workflow editor. Click on the **Event received** node and type the name of your event. You can reuse an existing event or create a new one from this input. For example, you can use a name like `sendDigest` and then click **+ Create new event**. Creating a trigger node Next, click on the **Edit event properties** button to add [properties](/docs/events/properties) to your event. Properties are extra pieces of data that you can attach to each event. This data is then made available in every email you send. In the event editor overlay, click **+ Add event property** and add any properties you want to include in your digest email. Adding event properties Make sure the "Trigger frequency" dropdown in the **Event received** node has **Every time** selected, so that each event triggers an email. ## Create your email The next step is to create the email you send to each contact. Click on the **Send email** node and then **Create email**. This will open the email editor, where you can [create your email](/docs/creating-emails/editor). When you want to add the event properties you created in the previous step, click the `⚡️` button above the editor (1) and then configure in the **Event Property** editor panel (2). Adding event properties in the editor In the right-hand panel, you will see a list allowing you to insert available event properties [into your email](/docs/creating-emails/personalizing-emails). Make sure to [add fallback values](/docs/creating-emails/personalizing-emails#fallback-values) for every property; if an event doesn't have a value for a property in your email, the email will not send. Fallback values make sure that emails are sent to every contact. When you've finished creating your email, click **Start** in the top right. This will make your workflow active and you can start triggering it by sending events. ## Trigger events To send events to Loops you can use an [integration](/docs/integrations), an [SDK](/docs/sdks) or [our API](/docs/api-reference/intro). You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. With the API, it's just a case of making a request to the [Send event endpoint](/docs/api-reference/send-event) containing the contact's details, the event name and your event properties. ```json theme={"dark"} { "eventName": "sendDigest", "email": "test@example.com", "eventProperties": { ... } } ``` ## Learn more Learn about creating email workflows. Read more about triggering emails with events. Learn how to add dynamic data to your emails. Find out how to send events using our API. # How to resend a campaign to new subscribers Source: https://loops.so/docs/guides/send-again A quick guide for sending a campaign to subscribers who signed up since you sent it out. If you're regularly adding new subscribers via [a form](/docs/forms/simple-form), [integration](/docs/integrations) or [the API](/docs/api-reference/intro), it's likely that you will want to send a campaign to new contacts who signed up after you sent it initially. In Loops, you can do this in two easy steps: duplicating the campaign and filtering the audience. ## Duplicate the campaign The first step is to duplicate the campaign so that you can send it out again. To do this, go to the Campaigns page and click on the `•••` menu icon, then select **Duplicate**. Duplicating emails ## Send to your new contacts When you're ready to send the email, it's important that it only gets sent to contacts that haven't been sent it already. To do this, on the **Audience** page of the sending flow, select "Not Sent" from the first dropdown and then the *original* campaign from the third dropdown. Filter audience by sent campaign This will update the total contacts and the table below, which will show contacts that were not sent the original campaign. Check that everything looks OK and continue sending the email. # Transactional vs marketing email Source: https://loops.so/docs/guides/transactional-vs-marketing-email When to use transactional email, when to use marketing email, and how Loops keeps the two separate so neither hurts the other. Misclassifying an email (sending marketing content through the transactional API, or a slow transactional message through a marketing path) hurts both deliverability and user expectations. This guide covers when each path is appropriate, what Loops does to keep them separate, and how to pick when you are not sure. For an overview of the three sending types in Loops (Campaigns, Workflows, Transactional), see [Types of emails](/docs/types-of-emails). ## Quick answer | Send it as... | When the email is... | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | [Transactional](/docs/transactional) | Triggered by a specific user action, expected by that user, and contains only content relevant to that action | | [Workflow](/docs/workflows) | Triggered by an event or property change, part of an ongoing lifecycle sequence | | [Campaign](/docs/types-of-emails#campaigns) | A one-off send to a segment of your Audience | Loops and Campaigns are both marketing email. Transactional is its own category. ## What counts as transactional [Transactional email](/docs/transactional#transactional-vs-marketing-emails) is 1-to-1 and triggered by the user. Clear examples: * Password resets and magic links * Email verification and OTP codes * Receipts, invoices, and payment confirmations * Shipping updates * Account creation and deletion confirmations * Security alerts ("new login from Chrome on macOS") * Scheduled report exports the user requested Less clear examples, with the typical call: | Email | Typical classification | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Welcome email right after signup | Marketing. Send as a [Workflow](/docs/workflows) on **Contact added**. The user signed up, but the email is relationship-building, not action-completing. | | Weekly [scheduled digest](/docs/guides/scheduled-digest-email) of their activity | Marketing, even though it is personalized | | "Your trial ends in 3 days" | Marketing, it is promoting renewal | | "Your trial ended, data will be deleted in 30 days" | Usually transactional, because it reflects account state and deletion timing the user needs | | [Team invitation](/docs/account/team-members) email | Usually transactional, the invitee took no action yet but it completes the inviter's action | | Re-engagement email | Marketing | When in doubt: if the user would reasonably be surprised to receive it, or if it contains any promotional content, send it as marketing. For individual cases you are unsure about, email [help@loops.so](mailto:help@loops.so). ## Why the distinction matters ### Sender reputation Transactional email has high engagement by default because the recipient is actively waiting for it. That engagement boosts your [sender reputation](/docs/deliverability/sending-reputation). Marketing email has lower engagement, and sending marketing content through a transactional path mixes the two signals and can hurt deliverability for the messages that matter. Loops sends transactional and marketing email from your verified sending domain and a small shared pool of Loops IPs by default, so the reputation signals are connected. This is usually helpful because high transactional engagement can support overall deliverability. It also means marketing content sent through a transactional path can weaken the same reputation profile that your operational messages depend on. ### Legal and compliance Marketing email must include an unsubscribe link and honor opt-outs. Transactional email is exempt because it is operationally required. Loops enforces this automatically: * Campaigns and Workflow emails include an unsubscribe link in the Loops footer, or you add `{unsubscribe_link}` when [uploading custom emails](/docs/creating-emails/uploading-custom-email#add-an-unsubscribe-url). * Transactional emails do not include an unsubscribe link. ### Unsubscribe vs suppression These are two different states in Loops, and the distinction matters when deciding classification: | State | What it means | Transactional still sends? | Marketing still sends? | | -------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------- | ---------------------- | | **Unsubscribed** ([`subscribed` property](/docs/contacts/properties#subscribed) is `false`) | The contact opted out of marketing | Yes | No | | **Suppressed** ([suppression](/docs/contacts/suppression)) | Hard bounce or complaint, Loops blocks all sends | No | No | So "transactional bypasses unsubscribes" is true. "Transactional bypasses suppression" is not, see [contact suppression](/docs/contacts/suppression). ### Tracking By default, Loops does not track opens or clicks on transactional email. This is deliberate, see [the note in the transactional guide](/docs/transactional#metrics). Marketing email tracks both. This means `email.opened` and `email.clicked` [webhook events](/docs/webhooks) do not fire for transactional sends. ## How Loops keeps them separate | Behavior | Campaigns and Workflows | Transactional | | ------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- | | Unsubscribe link | Required (auto-added or `{unsubscribe_link}` tag) | Not added | | Open and click tracking | On | Off | | Added to Audience | Yes, on signup or contact creation | Only with `"addToAudience": true` | | Triggers "Contact added" workflow | Yes | No | | Honors `subscribed: false` | Yes, does not send | No, still sends | | Honors [suppression](/docs/contacts/suppression) | Yes | Yes | | Sent via | Editor, [workflows](/docs/workflows), scheduled campaigns | [Send transactional email API](/docs/api-reference/send-transactional-email) only | Full behavior reference: [Transactional email overview](/docs/transactional). ## Common mistakes ### Sending a "welcome" email as transactional The user technically took an action (signing up), but a welcome email is usually promotional or onboarding content. Send it as a [workflow](/docs/workflows) with a "Contact added" trigger so it respects unsubscribes and contributes to your marketing engagement signal. For examples of what to include, see [Your first onboarding emails](/docs/guides/onboarding-emails). ### Sending marketing content through the transactional API Mixing product announcements or newsletters into transactional sends risks complaints and spam flags. Email service providers (ESPs) watch for this. The fix is to send marketing content as a [Campaign](/docs/types-of-emails#campaigns) or [Workflow](/docs/workflows) instead. ### Forgetting transactional still needs a [verified sending domain](/docs/sending-domain) Transactional uses the same domain setup as marketing. If DNS is not right, nothing sends. See [Setting up your domain](/docs/sending-domain). ### Using marketing-style "From" names on transactional Transactional should come from a From name the user recognizes from the app (for example, `Loops `). Overly branded or promotional From names on receipts and security alerts confuse users and hurt engagement. See [sending settings](/docs/creating-emails/sending-settings). ## Read more # Recipe: Upgrade upsell workflow Source: https://loops.so/docs/guides/upgrade-upsell Trigger an upsell sequence when a free-plan user hits a usage threshold that a paid plan would solve. Generic "upgrade now" emails get ignored. Specific ones ("you sent 950 of your 1000 monthly emails") convert. This recipe fires an upsell [Workflow](/docs/workflows) only when a user hits a usage threshold that a paid plan would remove, personalized to what they actually did. ## What you need * An event from your app when the user approaches a plan limit * A way to pass the usage context as [event properties](/docs/events/properties) * A [Stripe integration](/docs/integrations/stripe) is handy but not required ## Send the signal from your app When a free-plan user hits 80% of any plan limit, fire an event via the [Send event endpoint](/docs/api-reference/send-event): ```js REST theme={"dark"} await fetch("https://app.loops.so/api/v1/events/send", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.LOOPS_API_KEY}`, }, body: JSON.stringify({ email: user.email, eventName: "approachingPlanLimit", planName: "Free", eventProperties: { usageCurrent: 950, usageMax: 1000, usagePercent: 95, }, }), }); ``` ```ts SDK theme={"dark"} await loops.sendEvent({ email: user.email, eventName: "approachingPlanLimit", contactProperties: { planName: "Free", }, eventProperties: { usageCurrent: 950, usageMax: 1000, usagePercent: 95, }, }); ``` See [Events](/docs/events) and [Event properties](/docs/events/properties) for the full API. Note: this `approachingPlanLimit` event is specific to your app's usage tracking. The [Stripe integration](/docs/integrations/stripe) covers subscription, invoice, checkout, quote, and dispute events (see the full list in the integration doc), but usage-limit events still need to come from your own backend. ## Build the workflow In the [Workflow builder](/docs/workflows), pick **Event received** and select `approachingPlanLimit`. Add an [audience filter](/docs/workflows#audience-filters) to your workflow so you only send to free-plan users: `planName = Free`. Set the filter scope to **All following nodes**. Use [event properties](/docs/events/properties) directly in the email. In the editor, click the `⚡️` icon above the editor or type `{` and then the event property name: ``` You have sent {usageCurrent} of {usageMax} emails this month ({usagePercent}%). Upgrade to keep going. ``` Add a follow-up email to your workflow that is sent 3 days after the initial email. Your initial "All following nodes" audience filter will still be applied to the follow-up email. ## Why event-triggered beats scheduled Scheduled "here is our pro plan" campaigns hit people who aren't close to a limit and who did nothing to prompt the message. Event triggers mean every send goes to a user with a concrete reason to upgrade right now. If your pricing has multiple limits (users, storage, emails, integrations), fire a separate event type per limit, triggering a separate workflow for each. ## Measuring success * Open rate: should be higher than a generic upsell because the subject can cite the specific resource * Upgrade conversion: track via your existing billing system, not in Loops * Unsubscribe rate: watch this closely, over-sending upsells is a fast way to lose users ## Read more # What is BIMI? Source: https://loops.so/docs/guides/what-is-bimi Learn what BIMI is, how it works, and how to set up a BIMI record for your sending domain. BIMI (Brand Indicators for Message Identification) is an email standard that lets supporting inbox providers display your verified brand logo next to authenticated emails. ## What is BIMI? BIMI, or Brand Indicators for Message Identification is a relatively new email standard that allows you to add your brand’s logo to your authenticated emails. Adopting BIMI will allow your brand’s logo to appear next to your emails in your recipient’s inbox without manually needing to add and maintain it on a provider by provider level. As long as the email client supports BIMI, your logo will appear. ## Which email providers support BIMI? A growing number of email providers support BIMI. The current list of supported providers includes: * Gmail * Google Workspace * Apple Mail ([macOS Ventura 13, iOS 16, and iPadOS 16, or later](https://developer.apple.com/support/bimi/)) * AOL * Netscape * Fastmail * Yahoo (Yahoo Japan is not currently supported but is under considering for future adoption) * Pobox Here is the full breakdown of who does and doesn’t support BIMI as of right now: Infographic showing BIMI support by mailbox providers Image via [BIMI Group](https://bimigroup.org/bimi-infographic/) ## How does BIMI actually work? Setting up BIMI will take a bit more work than simply uploading your brand’s logo and hitting save. Technically speaking, BIMI is configured with a DNS TXT record (your “BIMI record”) that points inbox providers to your hosted logo (and optionally your Verified Mark Certificate). At a broad level, after you send an email, your recipient’s email provider verifies your email authentication (SPF, DKIM, and DMARC). If those checks pass, the provider can look up your BIMI record to fetch and display your logo next to the message. To start, the sender (you) will need to ensure that you are DMARC compliant. DMARC (domain-based Message Authentication, Reporting, & Conformance) is an [email authentication policy and reporting protocol](https://bimigroup.org/faqs-for-senders-esps/) that defends against unauthorized use of domains. Basically, DMARC helps protect your brand by detecting emails that aren’t coming from your domain – preventing spoofing and phishing attempts. The email provider will also run through the Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) protocols to ensure that the sender’s email address was sent from the correct domain. Next, you will need to create the logo that will actually be used. The current recommendation is a [square SVG file](https://bimigroup.org/creating-bimi-svg-logo-files/). The naming convention of this file should be: `https://yourservername.com/logo.svg` Next is an optional but recommended step. To fully embrace BIMI, your brand should acquire a VMC, or a Verified Mark Certificate. This will help validate the true ownership of the logo being used. More on this in the section below. The full implementation guide from BIMI can be located [here](https://bimigroup.org/implementation-guide/). ## BIMI requirements (high level) Before BIMI can work, inbox providers generally require that you have strong email authentication in place: * SPF configured for your sending domain * DKIM signing enabled * DMARC set up with an enforcement policy (often `quarantine` or `reject`) If you’re setting up sending in Loops, start with our [sending domain guide](/docs/sending-domain) and then review the deliverability docs (like [sending reputation](/docs/deliverability/sending-reputation)). ## How to set up BIMI (step-by-step) 1. Make sure SPF and DKIM are set up and passing for your sending domain. 2. Configure DMARC and move to an enforcement policy once you’re ready. 3. Create a BIMI-compatible SVG logo and host it at a public URL. 4. (Optional) Purchase a VMC if you want the strongest verification. 5. Add the BIMI TXT record to DNS (usually at `default._bimi.yourdomain.com`) and reference your hosted logo (and VMC if you have one). For example: ```txt theme={"dark"} v=BIMI1; l=https://yourdomain.com/logo.svg; a=https://yourdomain.com/vmc.pem ``` Notes: * `l=` is the URL to your BIMI SVG logo. * `a=` is optional and points to your VMC (Verified Mark Certificate). 6. Validate your setup using the [BIMI inspector](https://bimigroup.org/bimi-generator/). ## How much does BIMI cost? Along with taking some technical chops to get BIMI fully set up, it’s also not free. Getting the Verified Mark Certificate mentioned above currently [costs \$1,500](https://www.digicert.com/tls-ssl/verified-mark-certificates). On top of this cost, to qualify for the certificate your brand logo also needs to be a registered trademark, which will also come with additional costs and possible legal fees. ## Checking your BIMI record Now that you’ve gone through the work of setting up your BIMI records, it’s time to confirm that everything is working as expected. The BIMI group provides a [BIMI inspector](https://bimigroup.org/bimi-generator/) where you can enter your domain to ensure everything is set. ## Implement BIMI, build trust As you can see, fully embracing BIMI is a lengthy and potentially expensive process that may test your patience. However, leaning into this new email standard positions your brand to gain your reader’s trust while limiting the manual labor and upkeep on your end to ensure that your brand’s logo is always at the forefront of their inboxes. As the inbox becomes a more and more competitive landscape with each passing day, anything that you can do to stand out should be done. BIMI just might give your brand the edge it needs to capture those sought after eyeballs. # What is DNS? Source: https://loops.so/docs/guides/what-is-dns A beginner’s guide to DNS and the DNS records (MX, TXT, CNAME) used for sending email. Have you ever wondered how the internet seems to know precisely where to go when you type a website's name into your browser's address bar? How does your computer know to show you the right page when you type in `loops.so`? The answer to these questions lies in the Domain Name System or DNS. DNS is an essential part of the Internet's inner workings, serving as the Internet's equivalent of your iPhone’s address book. But what exactly is DNS, and why is it so important? To understand DNS, we first need to understand how the Internet works. Let’s dive in! ## What is DNS? Every device connected to the Internet, including computers, phones, and servers, has a unique IP address. An IP address is a string of numbers separated by periods that identifies each computer using the Internet Protocol to communicate over a network. For example, an IP address might look like this: "192.168.0.1". However, humans generally find it much easier to remember names rather than strings of numbers. This is where the **Domain Name System (DNS)** comes in. The DNS translates the domain names that we humans easily understand and remember into the IP addresses that computers use to identify each other on the network! For instance, when you type in `loops.so` into your web browser, your computer sends a request to a DNS server asking for the IP address associated with that domain name. The DNS server responds with the IP address, and your computer then sends a request to that IP address to fetch and display the website. The process of converting domain names into IP addresses is known as **DNS resolution**, and it usually takes only milliseconds. DNS servers are strategically located around the world and work together to ensure that these requests are processed quickly and accurately. ## Why is DNS important Now, why is DNS crucial? Firstly, it makes the internet user-friendly. Without DNS, we would have to memorize complex IP addresses for each website we wanted to visit. Secondly, it ensures the smooth operation of the internet. Every time you send an email, browse a website, or use an app, DNS is working behind the scenes to route your request to the right destination. And if that wasn’t enough, DNS also provides a level of security. DNS servers can filter and block access to certain websites that might be harmful or inappropriate, providing an essential layer of protection for internet users. It helps you verify the authenticity of a sender via email as we’ll cover in the next section. ## DNS and email DNS is vital for the functioning of email. ### Common DNS record types for email When you set up email sending (including with Loops), you’ll typically work with a few common DNS record types: * **MX records**: Tell the world which servers receive email for your domain. * **TXT records**: Used for many kinds of verification, including SPF and DMARC. * **CNAME records**: Used for domain verification and routing/aliasing in some setups. * **BIMI records**: A specialized TXT record used by supporting inbox providers to display your verified logo. Learn more in [What is BIMI?](/docs/guides/what-is-bimi). ### Sending email When you send an email, your email client needs to know where to send it. Let's say you're sending an email to `someone@gmail[.]com`. The client doesn't inherently know where "gmail.com" is, just like your web browser wouldn't know where to go if you entered that into the address bar. DNS steps in to resolve "gmail.com" into an IP address that represents the actual server your email needs to reach. ### Routing email to the right location DNS is used for email routing, particularly through a type of DNS record called the MX (Mail Exchanger) record. An MX record is a type of record that specifies a mail server responsible for accepting email messages on behalf of a recipient's domain. A priority value (a number like 10 is typical) is used to prioritize mail delivery if multiple mail servers are available. Without the MX records, your email wouldn't know which server to go to. ### Security Most importantly, DNS plays a critical role in email security. For instance, Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) are two methods used to prevent email spoofing, a technique used in phishing and spam campaigns where the sender masquerades as another by forging the header data. SPF and DKIM utilize DNS to hold text records that a recipient's server can check to verify the sender's identity. If you’re configuring your sending domain in Loops, start with our [sending domain setup guide](/docs/sending-domain) to add the required DNS records. In essence, DNS is a cornerstone for email operations and security. It helps your email find its way to the correct server, ensures the recipient's server can accept the mail, and provides mechanisms to verify the sender's identity to combat fraudulent activities. Without DNS, our email system would be far less reliable and secure. ## Send better email with Loops In summary, the Domain Name System, or DNS, is a critical component of both the internet and email as a whole, transforming the web from a complicated network of numeric addresses into an accessible and secure environment for users worldwide. It's the silent hero that allows us to navigate the digital world with ease, translating memorable website names into computer-friendly IP addresses. So, the next time you browse the internet or send that time-sensitive email, take a moment to appreciate the extraordinary system that makes it all possible: **DNS**. # Integrations Source: https://loops.so/docs/integrations Browse Loops integrations to sync contacts and trigger emails from around the internet. Loops integrations let you connect Loops to your product and your workflow tools so you can sync contacts, trigger workflows, and send transactional email. If you’re looking for the fastest way to get started, try [Zapier](/docs/integrations/zapier) or [Make](/docs/integrations/make). For custom setups, use [incoming webhooks](/docs/integrations/incoming-webhooks) or the [Loops API](/docs/api-reference/intro). ## Featured integrations Sync customers to your audience and send automated emails. Send Supabase authentication emails with Loops. ## Manage contacts } href="/integrations/auto-bcc" > Sync outgoing mail and contacts to Attio. } href="/integrations/bubble" > Manage contacts and send emails from your Bubble app. Sync users to and from your audience. } href="/integrations/fivetran" > Add contacts and contact properties from Fivetran (previously Census). } href="/integrations/framer" > Add a Loops signup form to your Framer site. Sync outgoing mail and contacts to HubSpot. } href="/integrations/integrately" > Manage contacts and send emails from over a thousand other platforms. } href="/integrations/make" > Manage contacts and send emails from thousands of other platforms. } href="/integrations/rudderstack"> Manage contacts and trigger workflows from hundreds of other platforms. } href="/integrations/posthog" > Sync users and trigger workflows from PostHog. Sync outgoing mail and contacts to Salesforce. } href="/integrations/segment" > Manage contacts and trigger workflows from thousands of other platforms. Sync customers to your audience and send automated emails. Sync Supabase users to your audience. } href="/integrations/webflow" > Add a Loops signup form to your Webflow site. } href="/integrations/zapier" > Manage contacts and send emails from thousands of other platforms. ## Send email } href="/integrations/auth0" > Send Auth0 authentication emails with Loops. Send Auth.js magic link emails with Loops. } href="/integrations/bubble" > Manage contacts and send emails from your Bubble app. } href="/integrations/integrately" > Manage contacts and send emails from over a thousand other platforms. } href="/integrations/make" > Manage contacts and send emails from thousands of other platforms. } href="/integrations/posthog" > Sync users and trigger workflows from PostHog. } href="/integrations/rudderstack" > Manage contacts and trigger workflows from hundreds of other platforms. } href="/integrations/segment" > Manage contacts and trigger workflows from thousands of other platforms. Sync customers to your audience and send automated emails. Send Supabase authentication emails with Loops. } href="/integrations/zapier" > Manage contacts and send emails from thousands of other platforms. ## Create templates Import custom MJML email templates from Emailify. Import custom MJML email templates from Email Love. # Auth0 integration Source: https://loops.so/docs/integrations/auth0 Send Auth0 authentication emails with Loops. Set up an SMTP connection to send all of your Auth0 emails with Loops. There are two big benefits to using Loops for sending your Auth0 emails: You can use [Loops' design editor](/docs/creating-emails/editor) to create (and then easily edit) beautiful transactional emails instead of having to code them with HTML. You get full visibility on which emails are being sent, when, and to whom in your Loops account. Auth0 doesn't offer this view. ## Set up Loops SMTP in Auth0 Go to **Branding -> Email Provider** in your Auth0 dashboard. Scroll down and click on **SMTP Provider**. In the SMTP Provider Settings section below, enter a value into the "From" field. This value will *always be overwritten by the values set in your Loops templates* from the next step, so it can be anything. In the **SMTP Provider Settings** section enter the following data: | Field | Value | | ----------- | ------------------------------------------------------------------------------------------- | | Host | `smtp.loops.so` | | Port number | `587` | | Username | `loops` | | Password | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops | Auth0 SMTP provider settings configured for Loops The **Send Test Email** button here will not work due to how the Loops SMTP system works. You can test your connection in a later step. ## Create Transactional emails in Loops Next, create new transactional emails for the emails you are sending from Auth0. Go to **Branding -> Email Templates** to view the full list. In Loops, go to the [Transactional page](https://app.loops.so/transactional) and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates). Creating a transactional email template in Loops You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like. You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails. For each Loops template you create, you need to [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Auth0 to be inserted into each email. You can check the list of [Common variables](https://auth0.com/docs/customize/email/email-templates#common-variables) supported in each email from the Auth0 documentation. Once you're done creating the email and adding the data variables, click **Next**. On the next page, click the **Show payload** button to view the API payload for your template. You will need this for the next step. Viewing the transactional email payload in Loops Make sure to also publish your email! It won't send unless it's published. Read our detailed guide for sending transactional emails. ## Configure email templates in Auth0 The final step is to make sure your emails in Auth0 are configured to send the correct data to Loops. Make sure you set up at least the **Verification Email (using Link)** or **Verification Email (using code)** templates in Auth0. Enable other emails based on your user flows. Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data. In Auth0, go to **Branding -> Email Templates**, then edit each template to contain the payload as shown in the previous step (you can click the clipboard icon in Loops to copy the full payload). Make sure that each template you want to use in Auth0 has the **Status** field enabled. If you are using Passwordless authentication, add your Loops payload into the **Body** field at **Authentication -> Passwordless -> Email**. Once pasted into the **Message** body, you need to add the Auth0 message variables into the payload. You can do this using double curly brackets like `{{ url }}`. Here is an example "Verification Email (using Link)" email template. This payload was copied from the template's Publish page in Loops, then the `{{ user.email }}` and `{{ url }}` Auth0 variables were added. ```json theme={"dark"} { "transactionalId": "clvmzp39u035tl50pw7wrl0ri", "email": "{{ user.email }}", "dataVariables": { "productName": "{{ application.name }}", "url": "{{ url }}" } } ``` If you want to add each Auth0 user to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ user.email }}` value. ```json {4} theme={"dark"} { "transactionalId": "clvmzp39u035tl50pw7wrl0ri", "email": "{{ user.email }}", "addToAudience": true, "dataVariables": { "productName": "{{ application.name }}", "url": "{{ url }}" } } ``` Here's how the template looks in the Auth0 editor: Auth0 email template editor containing the Loops payload JSON To test that everything works, click the **Try** button beneath the editor. Insert your email address in the modal that appears, then click **Try** to send the email. You will also be able to see activity for your email sends in **Monitoring -> Logs**. The best way to view your Auth0 email history is in Loops. Go to your [Transactional](https://app.loops.so/transactional) page then click on one of your emails. Click on **Metrics** in the left menu to view a page containing a table showing all sends and some statistics. ## Important notes * The subject in Auth0 templates is always overwritten by the subject added to the corresponding template in Loops. * The sender email configured in your Auth0 SMTP settings is always overwritten by the "From" address added to your templates in Loops. * Any enabled Auth0 template not set up with the correct API-like payload will fail to send. # Auth.js Source: https://loops.so/docs/integrations/authjs Send Auth.js magic link emails with Loops. Use Loops to send your [Auth.js Magic Link](https://authjs.dev/getting-started/authentication/email) emails. You must configure a [database adaptor](https://authjs.dev/getting-started/database) to use Magic Link authentication in Auth.js. There are two big benefits to using Loops for sending your Auth.js emails: You can use [Loops' design editor](/docs/creating-emails/editor) to create (and then easily edit) beautiful transactional emails instead of having to code them with HTML. You get full visibility on which emails are being sent, when, and to whom in your Loops account. Auth.js doesn't offer this out of the box. ## Create a transactional email In Loops, create a new transactional email, which will be sent to your users when they attempt to log in. Here is where you define your email's subject and sending details like From address and Reply to address. (Click the `>` button to reveal all the sending options.) Then you can create your email in the editor. We recommend creating a stylish but basic email for magic links, to help with deliverability. Use the [style panel](/docs/creating-emails/styles#style-panel) to customize the design. To add the Auth.js magic link in your email, add a [data variable](/docs/transactional#add-data-variables) named `url`. When the email is sent, each user's magic link will be inserted wherever you add the variable. You can add data variables as URLs to text, buttons and images. In this example, we're adding the `url` data variable into the button's link field. Make sure that your variable is named `url`, which is the variable name Auth.js uses when sending authentication emails. Magic link button When you're done make sure to Save and Publish your email. Unpublished emails will not be sent. ## Set up Loops in your Auth.js Project To use the Loops provider in Auth.js, you'll need to add two environment variables to your project. The first is an API key. You can generate or copy one from [Settings -> API](https://app.loops.so/settings?page=api). The second is the ID of your transactional email. Go to the email in Loops and click on to the **Publish** page. Here you will find the **Transactional ID**. Publish page with details Add both values to your environment, for example in an `.env` file: ``` AUTH_LOOPS_API_KEY= AUTH_LOOPS_TRANSACTIONAL_ID= ``` The final step is to configure your project to send emails with the built-in Loops provider. ```javascript auth.js theme={"dark"} import NextAuth from "next-auth" import Loops from "next-auth/providers/loops" export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: ..., // database adapter of your choosing providers: [ Loops({ apiKey: process.env.AUTH_LOOPS_API_KEY, transactionalId: process.env.AUTH_LOOPS_TRANSACTIONAL_ID, }), ], }) ``` That's it! Don't forget, your email details (subject, from address etc) and content can all be managed from Loops. You also get access to [transactional email logs](/docs/transactional#metrics) giving you an eye on your sending history. # Auto BCC Source: https://loops.so/docs/integrations/auto-bcc Import emails and contacts to your CRM by adding a BCC address to your marketing emails. Our Auto BCC feature works with campaigns and workflows, but is not offered on transactional emails. By adding a BCC email address in your settings, your emails will be BCCd to this address. Do not use a personal email address, only one generated by your CRM. ## Overview Auto BCC is a feature that imports emails and contacts into your CRM platform. This works by adding a BCC email address to all outgoing campaign and workflow emails sent from Loops. You can add a BCC email address in your [Sending settings page](https://app.loops.so/settings?page=sending). Be aware that BCCing might not be enough by itself. Some platforms may require you to add the sender email address to an allowlist or for the sender to be a user on your account. Auto BCC setting in Loops sending settings ## Syncing with Salesforce You can record all outgoing email and add contacts into Salesforce using a pre-generated Email to Salesforce address. [Find your BCC email address](https://support.cloudhq.net/where-do-i-find-my-bcc-address-in-salesforce/) and paste it into the **BCC email address** field in Loops. [Read how Email to Salesforce works](https://help.salesforce.com/s/articleView?id=sf.email_my_email_2_sfdc.htm\&type=5) ## Syncing with HubSpot You can log all outgoing emails and add new contacts to HubSpot using your pre-generated BCC address. [Find your BCC email address](https://knowledge.hubspot.com/connected-email/log-email-in-your-crm-with-the-bcc-or-forwarding-address#use-the-bcc-address) and paste it into the **BCC email address** field in Loops. Make sure to read the "Requirements to log emails to the CRM using the BCC address" section on the page above to make sure the sync works correctly. ## Syncing with Attio Using Attio's [Email forwarding](https://attio.com/help/reference/email-calendar/email-forwarding) feature, you can sync outgoing emails to your Attio account. [Find your forwarding email address](https://attio.com/help/reference/email-calendar/email-forwarding#finding-your-inbound-email-address) and paste it into the **BCC email address** field in Loops. Your Attio log in email has to match the sending email in Loops. For example, if your sending domain is `mail.mydomain.com` and the ["From" address](/docs/sending-first-email#sending-settings) for your email is `test@example.com`, your Attio account email has to be `test@example.com` for this integration to work. ## Syncing with other CRMs Most CRMs have a feature that lets you import email and contacts. Look for a BCC email address and add it to your Loops settings. # Better Auth Source: https://loops.so/docs/integrations/better-auth Use Loops as the email sender for Better Auth authentication emails. This guide shows how to use Loops to send [Better Auth](https://better-auth.com) authentication emails. For this guide, you will need a working app with Better Auth installed and a [Loops](https://app.loops.so) account with a verified sending domain set up. ## Create transactional emails in Loops The first step is to create transactional templates in Loops for each auth message. In this guide we have three example emails for the [Email & Password](https://better-auth.com/docs/authentication/email-password), [Magic Link](https://better-auth.com/docs/plugins/magic-link) and [Email OTP](https://better-auth.com/docs/plugins/email-otp) authentication methods. For the authentication method you are using, you should [create transactional emails](/docs/transactional) for these emails in Loops. Include [data variables](/docs/transactional#add-data-variables) for the data that will be passed from Better Auth. You can add data variables by typing "\{", via the `{}` button above the editor, or by using the `/` slash menu. Adding data variables to a transactional email After publishing each transactional email, note the **Transactional ID** from the **Review** page. You will need this for the next step. Copying the transactional ID The following variable names are suggestions and can be changed to whatever you want. Just make sure to use the same names in your Better Auth config code and in your Loops emails. ### Email & Password data variables Depending on the authentication method you are using, you will need to add different data variables to your emails. #### Verification email This email is sent when a user is requested to verify their email address. * `verificationUrl` * `token` #### Password reset email This email is sent when a user requests to reset their password. * `resetPasswordUrl` * `token` ### Magic link data variables This email is sent when a user signs in with a magic link. * `magicLinkUrl` * `token` ### Email OTP data variables #### Sign in email This email is sent when a user signs in using an email OTP. * `otpCode` #### Email verification email This email is sent when a user needs to verify their email using an email OTP. * `otpCode` #### Password reset email This email is sent when a user resets their password using an email OTP. * `otpCode` ## Add the Loops SDK Install the [Loops SDK](/docs/sdks/javascript) in your project. This will allow you to send transactional emails via Loops. ```bash theme={"dark"} npm i loops ``` Next you need to add some environment variables to your project, for example in an `.env` file. You can create an API key from your [API Settings](https://app.loops.so/settings?page=api) page. The transactional IDs are the IDs of the transactional emails you created in the previous step. (These are not all required, you will only need the ones for the authentication method you are using.) ```bash theme={"dark"} LOOPS_API_KEY=replace-with-your-loops-api-key # Email & Password LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID=replace-with-verify-template-id LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID=replace-with-reset-template-id # Magic Link LOOPS_MAGIC_LINK_TRANSACTIONAL_ID=replace-with-magic-link-template-id # Email OTP LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID=replace-with-email-otp-signin-template-id LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID=replace-with-email-otp-verification-template-id LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID=replace-with-email-otp-password-reset-template-id ``` ## Create a Loops server helper Next, create a helper function to keep email sending logic clean and reusable. This helper will be used in the next step to configure Better Auth to send emails through Loops. It is important that this helper is used server-side only. Never expose your Loops API key in client-side code. ```typescript lib/loops.ts theme={"dark"} import { LoopsClient } from "loops"; const loops = new LoopsClient(process.env.LOOPS_API_KEY as string); type DataVariables = Record; export async function sendLoopsTransactionalEmail(input: { email: string; dataVariables?: DataVariables; }) { const { transactionalId, email, dataVariables } = input; transactionalId: string; const response = await loops.sendTransactionalEmail({ transactionalId, email, dataVariables, }); if (!response.success) { throw new Error(response.message || "Failed to send Loops transactional email"); } } ``` ## Configure Better Auth to send through Loops In your Better Auth config, enable the authentication method you want to use and add callbacks to send emails. Inside the `sendLoopsTransactionalEmail()` calls, we populate the content for the data variables you added in your Loops emails. Make sure that the data variable names inside `dataVariables` match the names of the data variables you added in your Loops emails. Better Auth recommends not awaiting email sends in these callbacks to reduce timing-attack risk and keep auth responses fast, so we use `void`. If your platform supports background work (for example `waitUntil`), you can use that instead. ### Password reset For this email, make sure that `emailAndPassword.enabled` is set to `true`, then add a `sendResetPassword` callback containing our `sendLoopsTransactionalEmail()` helper function. [Better Auth docs](https://better-auth.com/docs/authentication/email-password#request-password-reset) ```ts auth.ts {8-15} theme={"dark"} import { betterAuth } from "better-auth"; import { sendLoopsTransactionalEmail } from "./lib/loops"; export const auth = betterAuth({ emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url, token }) => { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_RESET_PASSWORD_TRANSACTIONAL_ID as string, email: user.email, dataVariables: { resetPasswordUrl: url, token, }, }); }, }, }); ``` ### Email verification For this email, add a `sendVerificationEmail` callback containing our `sendLoopsTransactionalEmail()` helper function. [Better Auth docs](https://better-auth.com/docs/authentication/email-password#email-verification) ```ts auth.ts {7-14} theme={"dark"} import { betterAuth } from "better-auth"; import { sendLoopsTransactionalEmail } from "./lib/loops"; export const auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url, token }) => { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_VERIFY_EMAIL_TRANSACTIONAL_ID as string, email: user.email, dataVariables: { verificationUrl: url, token, }, }); }, }, }); ``` ### Magic link plugin Add the `magicLink` plugin to your Better Auth config and add a `sendMagicLink` callback containing our `sendLoopsTransactionalEmail()` helper function. [Better Auth docs](https://better-auth.com/docs/plugins/magic-link) ```ts auth.ts {2,9-16} theme={"dark"} import { betterAuth } from "better-auth"; import { magicLink } from "better-auth/plugins"; import { sendLoopsTransactionalEmail } from "./lib/loops"; export const auth = betterAuth({ plugins: [ magicLink: { sendMagicLink: async ({ email, token, url }) => { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_MAGIC_LINK_TRANSACTIONAL_ID as string, email, dataVariables: { magicLinkUrl: url, token, }, }); }, }, ], }); ``` ### Email OTP plugin Add the `emailOTP` plugin to your Better Auth config and add a `sendVerificationOTP` callback containing our `sendLoopsTransactionalEmail()` helper function. There are three types of OTP emails: sign in, email verification and password reset, so you will need to create an email template for each type in Loops. [Better Auth docs](https://better-auth.com/docs/plugins/email-otp) ```ts auth.ts {2,10-16,18-24,26-32} theme={"dark"} import { betterAuth } from "better-auth"; import { emailOTP } from "better-auth/plugins"; import { sendLoopsTransactionalEmail } from "./lib/loops"; export const auth = betterAuth({ plugins: [ emailOTP: { async sendVerificationOTP({ email, otp, type }) { if (type === "sign-in") { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_OTP_SIGNIN_TRANSACTIONAL_ID as string, email, dataVariables: { otpCode: otp, }, }); } else if (type === "email-verification") { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_OTP_VERIFICATION_TRANSACTIONAL_ID as string, email, dataVariables: { otpCode: otp, }, }); } else { void sendLoopsTransactionalEmail({ transactionalId: process.env.LOOPS_OTP_PASSWORD_RESET_TRANSACTIONAL_ID as string, email, dataVariables: { otpCode: otp, }, }); } }, }, ], }); ``` ## Call Better Auth from your app Now your app is set up to send Better Auth emails through Loops. For example, Loops will send a transactional when you [sign in a user with an email OTP](https://better-auth.com/docs/plugins/email-otp#sign-in-with-otp): ```ts theme={"dark"} const data = await auth.api.sendVerificationOTP({ body: { email: "test@example.com", // required type: "sign-in", // required }, }); ``` Or when you [reset a user's password](https://better-auth.com/docs/authentication/email-password#api-method-request-password-reset): ```ts theme={"dark"} const data = await auth.api.requestPasswordReset({ body: { email: "test@example.com", // required redirectTo: "https://example.com/reset-password", }, }); ``` ## Troubleshooting If your emails are not sending: * Confirm each transactional email in Loops is **Published** * Confirm each data variable name in your Better Auth code matches the name of the data variable in your Loops email * Check that you're using a valid Loops API, from the correct account * Check that your sending domain is verified in Loops ## Read more Loops API endpoint used to send transactional emails. Read our guide for sending emails from Bolt.new. Integrate Loops into lots more platforms. # Bubble integration Source: https://loops.so/docs/integrations/bubble Connect Bubble to Loops with our plugin to add contacts, trigger workflows, and send transactional email. The Loops Bubble integration connects your Bubble app to Loops so you can sync contacts, trigger workflows, and send transactional email. For other Loops integrations, see the [integrations overview](/docs/integrations). Our Bubble plugin lets you: * Add, find, update and delete contacts * Send events to trigger workflows * Send transactional email Our Bubble plugin is unfortunately limited to what it can do because of how Bubble works. If you want more flexibility, such as syncing more contact data to Loops, check out our [tutorial for using Bubble's API Connector](/docs/guides/bubble-api-connector). ## Install the plugin Go to the [Loops plugin](https://bubble.io/plugin/loops-1704117562175x705056703666192400) and select your app from the "Install in an application..." dropdown. Installing the Loops plugin from Bubble Alternatively, go to the Plugins page in your Bubble admin, click "Add plugins" in the top right, and search for "Loops". Searching for the Loops plugin in Bubble ## Set up the plugin To use the plugin you will need to add a Loops API key into Bubble. First, go to your [Loops API settings page](https://app.loops.so/settings?page=api) and copy an API key. You may need to create a key first. Then go to the Plugins page in Bubble, select the Loops plugin and paste your API key into the "API key" field, prepended with the word "Bearer" and a space. Adding a Loops API key to the Bubble plugin settings ## Using the plugin actions To use the plugin actions in a workflow, select "Plugins" in the menu and you will see the options show up (prefaced with "Loops - "). Selecting a Loops action in a Bubble workflow Here's a simple example of using the Bubble plugin to add all new users to your Loops audience. After your sign up action, add a new "Loops - Create a contact" action. In the form, add your user email into the "Email" field and user ID into the "User ID" field. Example Bubble workflow action to create a Loops contact And that's it! ## Actions The plugin contains actions that replicate what's possible with the [Loops API](/docs/api-reference/intro). ### Create a contact The **Create a contact** action will create new contacts in Loops using the email address and user ID that you send from Bubble. If you want to "create or update" users, use the [Update a contact](#update-a-contact) action instead. You need to provide both email and user ID values, otherwise the underlying API request from Bubble will fail. The **Create a contact with name** action lets you also send a first and last name to Loops. [API documentation](/docs/api-reference/create-contact) ### Find a contact The **Find contact by email** action will find a Loops contact based on the provided email address. This can be used to see if one of your user already exists in your Loops audience. Similarly the **Find contact by user ID** action will find a contact by their ID. [API documentation](/docs/api-reference/find-contact) ### Update a contact This action will update the email address of a Loops contact who has the provided user ID. This action can also be used to "update or create" contacts. If a contact doesn't already exist with the provided email or user ID, a contact will be created. The **Update a contact with name** action lets you also send a first and last name to Loops. [API documentation](/docs/api-reference/update-contact) ### Delete a contact The actions **Delete a contact by email** and **Delete a contact by user ID** will delete a contact from Loops. This is useful for when a user closes their account in your application and therefore you no longer want to email them from Loops. [API documentation](/docs/api-reference/delete-contact) ### Send an event The send event actions can be used to [trigger workflows](/docs/workflows) from your application. For example, you may have a welcome workflow which sends an email drip campaign to new users. For the Send an event action, you need to provide an "Event name" value and the user's ID or email address. Bubble workflow action fields for sending an event to Loops [API documentation](/docs/api-reference/send-event) ### Send transactional email Transactional emails are one-time emails like password resets sent by apps to users based on an action. To send transactional emails, you will first need to [create the email in Loops](/docs/transactional#compose-your-email). Once you have written the email and added data variables, you can click **Next** to see the example payload for the API. Note the names of the `dataVariables` (which you added in the email) because you need these in Bubble. Bubble plugin action fields for sending transactional email Once you create the action in Bubble, you will see the three fields in the form. Here is an example API payload for the Loops API: ```json theme={"dark"} { "transactionalId": "clomzp89u635xl30px7wrl0ri", "email": "test@example.com", "dataVariables": { "lastLoggedIn": "20240214T10:01:29Z", "plan": "Pro" } } ``` To get the same effect in Bubble, we need to enter the following into the "Data variables" field: `"lastLoggedIn": "20240214T10:01:29Z", "plan": "Pro"` To add user data into this field, place your cursor and select **Insert dynamic data** (see image above). [API documentation](/docs/api-reference/send-transactional-email) # Carrd integration Source: https://loops.so/docs/integrations/carrd Enable sign ups to Loops using a native Carrd form. ### Add a form to your Carrd site carrd image 1. Add a form to your site and select the **Custom** option. 2. Select **Send to URL** and paste in your form endpoint from the [Forms page in Loops](https://app.loops.so/forms). form endpoint 3. Paste the form endpoint you copied from into the **URL** input in Carrd. 4. Change the **Method** to "AJAX" and the **Format** to "JSON". Our form submission endpoint has rate limiting, so you will see an error in testing if you submit more than once per minute or submit the same email twice. ### Customizing the form In addition to collecting the email address, you can also collect any other contact property you want. 1. Follow Carrd's documentation to [add a new field to your form](https://carrd.co/docs/forms/setting-up-a-custom-form) 2. Determine your preference: * A hidden field that is set to a static value * A value that your user can set 3. Assign the field an "ID" matching the Loops API property name that you want to set. You can check the full list of your available properties from your [API Settings](https://app.loops.so/settings?page=api) page. For example, if you want to set the User Group property, you would add a hidden field and set the ID to `userGroup`. To add subscribers to specific [mailing lists](/docs/contacts/mailing-lists), add a field with an ID `mailingLists`. The **Value** can be a single mailing list ID or if you want to add subscribers to multiple lists, a comma-separated list of IDs. Mailing lists need to be marked **Public** in your [Lists settings](https://app.loops.so/settings?page=lists) in order for them to work in forms. # Clay integration Source: https://loops.so/docs/integrations/clay Learn how to sync data between Clay and Loops. Clay is a platform for managing and enriching your customer and user data. You can sync contact data between Clay and Loops using webhooks and API requests, as well as trigger emails from Clay. ## Send Loops contacts to Clay You can send Loops contact data to Clay using Loops [webhooks](/docs/webhooks). ### Create a webhook in Clay In Clay, add a Webhook source by clicking **More sources...** in the sidebar and selecting **Webhook**. Add a webhook in Clay This generates a new webhook URL. Make sure to change the **Send response as** option just below the URL to "JSON" to match the data format sent by Loops. Webhook setup in Clay ### Set up the webhook in Loops Copy the URL and paste it into the **Endpoint URL** field on the [Webhooks](https://app.loops.so/settings?page=webhooks) settings page in Loops. Activate `contact.created` events. This is the only event that makes sense to sync to Clay because it's only [event type](/docs/webhooks#event-types) that contains a full contact record. Webhook in Loops ### Create a data mapping in Clay Now, in order to create data mappings in Clay, you need to send some data from Loops. To do this we need to create a contact, which will trigger a webhook to be sent to Clay. In your Loops [Audience page](https://app.loops.so/audience) you can create a new contact from the `+` button in the top right, or use the [API](/docs/api-reference/create-contact) or an [integration](/docs/integrations). Once the contact is created, go back to Clay. You should see a webhook record in your table, and there should now be data shown in the **Setup mapping** section on the right. Event received in Clay Now you can map data from Loops to columns in Clay. Click on the cell in the **Webhook** column. On the right you can click on attributes and map them to columns. Expand the **Contact** object to view the full record from Loops. Click on an attribute you want to sync and then **Add as column**. You have the option to map the data to a new or existing column. Mapping data in Clay Once you've mapped your desired data to Clay, future webhooks will automatically sync the same Loops data to your Clay table. You can test this by adding another contact to your Loops audience. If you ever want to check or view the data coming into Clay, clicking on cells in the **Webhook** column will show the full request body for each request. ## Send Clay contacts to Loops You can sync data to your Loops audience from Clay using a custom enrichment in your tables. This sends contact data to Loops whenever a row is created or updated (or on a manual schedule). ### Create a connection to the Loops API Add an API connection to your table in Clay by selecting **Add enrichment** and searching for **HTTP API** as the data source. In the **Account** section of the sidebar, click **+ Add account** or select an existing connection. Add a connection If you're adding a new connection, give the connection a name (e.g. "Loops") and then add the two API Request Headers listed below. You'll need to generate or copy an API key from [Settings -> API](https://app.loops.so/settings?page=api) in Loops and replace `` with the key. | Key | Value | | :------------ | :----------------- | | Authorization | `Bearer ` | | Content-Type | `application/json` | Click **Save** to create the connection. Connection details If you want to edit the connection in the future, you can do so from **Settings -> Connections** in Clay. ### Create an API request We're going to set up an "update contact" API request, which will *create or update* contacts in Loops. In the **Configure** section, select "PUT" from **Method** dropdown and enter the following URL into the **Endpoint** field: ``` https://app.loops.so/api/v1/contacts/update ``` In the **Body** field, you can build up the request, using data from your table. Type `/` to add columns, and make sure to wrap values in quotes to create valid JSON data for the API. You must include an `email` in the request body. API request body Data will be sent to Loops automatically when a row is created or updated when the **Auto-update** option is enabled. Toggle this off to only sync data manually. To edit the request in the future, click on the **HTTP API** column header and select **Edit column**. Read the API documentation for updating contacts ### Test the setup To test the connection and request you created, click **Save** and then **Save and run (x) rows in this view**. If the sync succeeds, you'll see the cell populate with "200", which means a successful API request was made. 200 means success If there is an error sending data to Loops, you can click on the cell in the **HTTP API** column to see the error message returned by the API, including the reason for the failure. ## Send events from Clay You can trigger [workflows](/docs/workflows) from inside Clay by [sending events](/docs/events) with the Loops API. ### Create an event and a workflow An [event](/docs/events) allows you to start a workflow when something happens in an external platform (like Clay). Workflows can contain emails, timers, and filters. Learn more about events Learn more about workflows To define your event, go to [Settings -> Events](https://app.loops.so/settings?page=events) and click **Create**. You can specify [event properties](/docs/events/properties) for the event, which are data about specific events that you can add into your emails to personalize them for each recipient. Create an event When you're done, go to the [Workflows page](https://app.loops.so/loops) to create a new workflow. Use the "Event received" trigger and select your event from the previous step. In this workflow, add as many emails as you want plus timers to space them out. You can personalize emails by [adding event properties](/docs/events/properties#using-event-properties-in-emails) into your emails body, subject and other sending settings fields. Workflow email ### Create an API request To send an event with the API from Clay, we need to make a request containing our event data. If you've added event properties to your emails, we need to include those in the request. Add a new enrichment to your table in Clay by selecting **Add enrichment** and searching for **HTTP API** as the data source. Then create or select an existing connection to the Loops API. Follow the [steps outlined above](#create-a-connection-to-the-loops-api). In the **Configure** section of your HTTP API enrichment, select "POST" from **Method** dropdown and enter the following URL into the **Endpoint** field: ``` https://app.loops.so/api/v1/events/send ``` In the **Body** field, you can build up the request, using data from your table. Type `/` to add columns, and make sure to wrap values in quotes to create valid JSON data for the API. You must include an `eventName` and an `email`/`userId` in the request body. [Read more](/docs/api-reference/send-event#body) API request body You can also update contact information in Loops in the same request by including contact properties alongside `email` in the root of the request body. [More info](/docs/api-reference/send-event#contact-properties) Events will be sent to Loops automatically when a row is created or updated with the **Auto-update** option enabled. Toggle this off to only sync data manually. You can use the **Only run if** option to only send events when a certain condition is met. Conditional sending To edit the request in the future, click on the **HTTP API** column header and select **Edit column**. Read the API documentation for sending events ### Test the setup Review the [information in the previous section](#test-the-setup) to see how to test your setup # Clerk integration Source: https://loops.so/docs/integrations/clerk Sync contacts and send emails triggered by events in Clerk. Our Clerk integration lets you: * Create and update contacts * Send events to trigger workflows Our Clerk integration is built on top of our [Incoming webhooks](/docs/integrations/incoming-webhooks) feature. This system lets you send webhooks from supported platforms directly to Loops so you can easily sync users and customers as well as send automated emails. [Please read our guide about incoming webhooks](/docs/integrations/incoming-webhooks) With Clerk, you can keep your user data synced to Loops so you can send emails to your userbase. ## Supported events We accept the following Clerk events: * `user.created` * `user.deleted` * `user.updated` * `waitlistEntry.created` * `waitlistEntry.updated` [Clerk webhook docs](https://clerk.com/docs/webhooks/overview) If you send other events, they will be ignored. If you would like to see more events supported, please let us know by sending a message to [our support team](https://app.loops.so/settings?page=support). Please keep in mind only events that contain an email address are able to be processed. ## Synced data For `user.created` or `user.updated` events, we sync the following Clerk data to your Loops contacts: * User ID * Email address * First name (optional) * Last name (optional) We use the IDs of Clerk users to match contacts in your Loops audience. If the user ID is not found in Loops, we will create a new contact. The `user.deleted` event can be used to delete or unsubscribe your Clerk users from your Loops audience. For `waitlistEntry.*` events, we sync: * Email address There is no user ID available for waitlist events. When a user graduates from your waitlist to being a user (i.e. you accept them and they complete their account), their user ID will be added to your contact in Loops. ## Create a webhook endpoint in Loops [Follow the instructions here](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops) to create a new webhook endpoint, which will allow you to send webhook events directly to Loops. Endpoint form ## Create a webhook in Clerk Next, you need to set up webhooks in Clerk. Inside a project in Clerk, go to **Configure -> Webhooks** and click **+ Add Endpoint**. Paste in the endpoint URL from Loops, then select the event(s) you want to send (see our [supported events](#supported-events) above). Adding a webhook in Clerk Click **Create** to finish. The last step is to copy the signing secret into Loops. On the webhook page in Clerk, click the eye icon on the right to show the secret in the page. Copy the secret and paste it into the **Signing Secret** field in Loops. Reveal Clerk secret Now you're all set up. ## Testing Clerk webhooks Clerk offers a nice way to test webhooks. Click through to the webhook you created and then the **Testing** tab. Select `user.created` or `user.updated` from the **Send event** dropdown, tweak the example data that's shown, then click **Send Example**. This will send real data to your Loops account. You can also test your webhook by creating and editing users in the **Users** page in Clerk, or by signing up in your application. You can see all sent webhooks by going to **Webhooks**, clicking on the webhook and scrolling down to the **Message Attempts** section. On Loops' end, you will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page. ## Examples Here are some examples of how you can send data from Clerk to Loops to sync contacts and trigger useful emails to your customers. ### Syncing users to Loops Create or update contacts in your Loops audience when a user is created or updated in Clerk. 1. Create a new Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops)). 2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.created` and `user.updated` events and paste in your endpoint's URL. 3. In Loops, make sure `user.created` and `user.updated` are toggled on on the Clerk settings page. ### Syncing waitlist signups to Loops Create or update contacts in your Loops audience when someone joins your waitlist in Clerk. 1. Create a new Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops)). 2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `waitlistEntry.created` event and paste in your endpoint's URL. 3. In Loops, make sure `waitlistEntry.created` is toggled on on the Clerk settings page. 4. To keep track of waitlist users you can use the **Assign a user group** option. Enter something like "Waitlist" into field. ### Send an email to all new Clerk users Send an email from Loops when a new user is created in Clerk. 1. Set up your Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops)). 2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.created` event and paste in your endpoint's URL. 3. In Loops, make sure `user.created` is toggled on on the Clerk settings page. 4. Create a new workflow in Loops using our **Onboarding drip** template. 5. For the workflow trigger, select **Event received** and then select **Clerk** from the first dropdown and **user.created** from the second dropdown. ### Enter all new Clerk users into an onboarding email sequence Send an email from Loops when a new customer is created in Clerk. 1. Complete Steps 1–5 from "Send an email to all new Clerk users" above. 2. Add more emails and timers into the workflow to complete your email sequence. ### Send an email to a deleted Clerk user Send an email from Loops when a user is deleted in Clerk. 1. Set up your Clerk webhook endpoint in Loops ([instructions](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops)). 2. In Clerk, create a new webhook ([instructions above](#create-a-webhook-in-clerk)) for the `user.deleted` event and paste in your endpoint's URL. 3. In Loops, make sure `user.deleted` is toggled on on the Clerk settings page. 4. Create a new workflow in Loops. 5. For the workflow trigger, select **Event received** and then select **Clerk** from the first dropdown and **user.deleted** from the second dropdown. 6. Add an email to the workflow. 7. You can optionally delete or unsubscribe the user from your audience from the Clerk settings in Loops. Note that both options will stop that contact from receiving future emails from Loops. # Fivetran integration Source: https://loops.so/docs/integrations/fivetran Send user data from your data warehouse to Loops via Fivetran. This integration partner was previously called Census. Our Fivetran integration lets you: * Create and update contacts * Trigger workflows when contacts are created or updated Loops is a partnered destination for Fivetran. We support syncing data via `userId` or `email`. New contacts cannot be created if no email is provided. Within the Fivetran app, go to **Destinations** and click **+ New Destination**. Select the **Loops** destination. Add Loops destination You will be prompted to enter your Loops API Token, which you can find on your [API settings page](https://app.loops.so/settings?page=api), and to decide if you want to trigger workflows when contacts are created or updated. Create a destination Click **Connect** and test out your destination. Loops will now be available as a destination in Fivetran. Use the Loops destination When choosing a sync behavior, you can choose either **Update or Create** or **Mirror**. Choose sync behavior # Framer integration Source: https://loops.so/docs/integrations/framer Enable signups from your Framer site using an in-built or custom Loops component. Collect new subscribers from your Framer site. There are a few ways to set this up. 1. [Framer Form component](#framer-form-component)\ This uses the native Form component in Framer and gives good flexibility for extra form fields. 2. [Framer Loops component](#framer-loops-component)\ This simplest method. A simple drop-in form with an email address input. 3. [Custom component with code](#advanced-integration)\ The most flexible but complex option, using custom code. Our form submission endpoint has rate limiting, so you will see an error in testing if you submit more than once per minute or submit the same email twice. ## Framer Form component Use [Framer Forms](https://www.framer.com/features/forms/) to easily create a form on your site. ### Insert the Form Component From **Insert -> Forms** drag the **Form builder** component into your page. This will add an example form. Framer Form component ### Edit the form fields Edit the fields in the form to match the data you want to collect from new subscribers. An email field is required. Make sure to toggle the **Required** option to **Yes** for your email field. You need to edit the **Name** value of each field to match the [contact properties' "API name"](/docs/contacts/properties) in Loops. For example, the **Name** value must be "email" for the email address field. Email form field You can add hidden fields to populate data like `mailingLists`, `userGroup` and `source` in Loops yet ensure they don't show up in the form. You may have to click the `+` button to show the **Value** and **Hidden** options. To add subscribers to specific [mailing lists](/docs/contacts/mailing-lists), add a field with **Name** `mailingLists`. The **Value** can be a single mailing list ID or a comma-separated list of IDs. Mailing lists need to be marked **Public** in your [Lists settings](https://app.loops.so/settings?page=lists) in order for them to work in forms. Hidden form field ### Configure the form The final step is to set the endpoint to your Loops form URL. 1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account. 2. Click on the **Settings** tab. 3. Copy the **Form Endpoint**. Form endpoint 4. Back in Framer, select your form. In the **Send To** option, select "Webhook". Then paste the URL from Loops into the **API** field. Framer form webhook Now your form is all set up and can start receiving new subscribers. ### Set up a confirmation message By default a "Thank you" message is shown inside the form button when the form is submitted successfully. You can opt to use a redirect instead. You can add a confirmation message on another web page and use the **Redirect** option in the form settings. ## Framer Loops component Framer has a built-in Loops option for creating simple signup forms with an email address field. ### Insert the Loops Component From **Insert -> Forms** drag the **Loops** component into your page. This will add an example form. Framer Loops component ### Configure the form Next, you need to add your Loops form ID to the **ID** field. 1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account. 2. Click on the **Settings** tab. 3. Copy the **Form ID**. Form ID 4. Paste this ID into the **ID** field in the Framer component. Loops form ID 5. Framer offers two extra fields in the component: * **User Group** populates the contact's `userGroup` value in Loops. * **Mailing list** can be used to subscribe contacts to your [mailing lists](/docs/contacts/mailing-lists). Add here one or more mailing list IDs separated by commas. Mailing lists need to be marked **Public** in your [Lists settings](https://app.loops.so/settings?page=lists) in order for them to work in forms. ### Set up a confirmation message Make sure to also set up a confirmation message by clicking on the **Success** dropdown. You can choose to show a message in an overlay with the "Show Overlay" option or redirect the user to another web page with the "Open Link" option. To add an overlay message, click on the **Overlays** section header in the right-hand panel, choose between "Relative" and "Fixed" and make sure the **Show On** selection is "Submit". ## Advanced integration This option adds a custom component into your Framer site [using form code](/docs/forms/custom-form) generated by Loops ### Generate the form code 1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account. 2. Click on the **Settings** tab. 3. Select “JSX” from the **Generate Form Code** dropdown (1), then copy the code snippet (2). Generating JSX form code in Loops ### Embed the component in Framer 1. Create a new component. Toggle over to **Assets** in the Framer side panel then click the `+` button. Creating a new Framer component from the Assets panel 2. Give your New Component a title. Naming the new component in Framer 3. Finally, paste the code copied in from Loops into the code editor. You should see the Preview on the right fill in with a preview of your component. Pasting Loops form code into the Framer code editor 4. Drag and drop your new asset anywhere on your page to use it :) # Incoming webhooks Source: https://loops.so/docs/integrations/incoming-webhooks Send data to Loops from supported platforms using webhooks. Incoming webhooks allow you to: * Create and update contacts * Send events to trigger workflows This feature lets external platforms send webhook events directly to Loops, making it straightforward to create or update contacts in Loops automatically when changes happen in other platforms. You can also trigger events when webhooks arrive in Loops so you can send automated email after something happens in your other accounts. ## How it works First, you create webhook endpoints in your Loops account. These allow other platforms to send data automatically and directly to Loops. You then create webhooks in the external platforms, which send event data to your Loops endpoint URLs. Note: we only process webhook events listed for each provider (and which contain an email address). We return helpful messages in responses if there is an issue processing a webhook event. Check the webhook logs in your external platforms for more details. For each type of webhook event, you can sync customer data like names and assign user groups, as well as trigger workflows via [events](/docs/events). Incoming webhook configuration ### Syncing contacts The primary use case for incoming webhooks is to create and update contacts in your Loops audience. When data arrives in Loops, we grab the email address to create and update contacts in your Loops audience. To this end, we only support incoming events that contain an email address. You can choose to update first and last name data from the webhook event as well. Additionally, you can assign a user group value to each new contact, which helps create segments from webhook-created contacts. Any new contact created via a webhook will have a source like "Stripe webhook" so you know where it originated from. For events that reference record deletion, like Stripe's `customer.deleted` event, you can choose to unsubscribe or delete contacts in Loops. ### Subscribing to mailing lists You can subscribe contacts to [mailing lists](/docs/contacts/mailing-lists) when they are created or updated via a webhook. ### Sending emails Incoming webhooks can trigger emails if you connect events to [workflow triggers](/docs/workflows/triggers). This can be useful if you want to automatically send emails when something has happened in the external platform, for example a successful payment in Stripe or a new sign up in Clerk. Just create a workflow using the **Event received** trigger and select the event you want to trigger on. Event received trigger You can also trigger custom events from incoming webhooks if you specify an event in the **Trigger an additional custom event** section in the configuration. ## Create webhook endpoints in Loops To start sending webhook events to Loops, go to your chosen [integration's settings page](https://app.loops.so/settings) in Loops. A webhook endpoint will be created for you. Copy the endpoint URL and paste it into your external platform. For Stripe, install the Loops app, which lets you skip the creating of webhooks as it's handled for you. Endpoint form You may need to copy-paste signing secrets between the platform and Loops for extra security (we will prompt you when this is necessary and give you the steps to do it). ## Supported platforms Learn more about how to configure webhooks for each platform, and see which events we support, below. # Integrately Source: https://loops.so/docs/integrations/integrately Connect Loops to over a thousand apps. Our Integrately integration lets you: * Create, find, update and delete contacts * Send events to trigger workflows * Send transactional email ## Create an automation In Integrately, create an automation by searching for and selecting the two platforms you want to connect. Add an automation in Integrately You'll need to connect to or sign in to each platform. When it comes to connecting to Loops, you need to create or copy an API key from [your API Settings page](https://app.loops.so/settings?page=api). Paste your API key into Integrately. Add Loops API key Now you are connected to Loops and can continue setting up the automation. ## Manage contacts To sync contacts, choose either the "Create contact" or "Create or update contact" action. You can map data between the two platforms in the automation form. In this example, the "Email" field contains the data from the "Email" column in Google Sheets. Create a contact automation ## Send event To send events from Integrately (i.e. to trigger a [workflow](/docs/workflows)), choose the "Send event" action and then map an "Email" and "Event name" in the available fields. Send event automation ## Send transactional email To send transactional email from Integrately, select the "Send transactional email" action. Map the email address you want to send to. Then add the ID of your transactional email from Loops (found on the "Publish" page of the transactional email editor). Finally, add your email's data variables. You can easily map dynamic variables from the source app or add static text in the form. Send transactional email automation # Make integration Source: https://loops.so/docs/integrations/make Utilize our official Make integration to manage contacts and send email. Our Make integration lets you: * Create, find, update and delete contacts * Send events to trigger workflows * Send transactional email ## Create a connection Before you get started, head over to the API page and [create a new API key](https://app.loops.so/settings?page=api) in Loops. You'll need to copy the API key and paste it into Make. To get started create a new scenario and select the "Loops" module. Adding loops in Make Then select the "Create Connection" button and paste in your API key in the following screen. Create a connection with Make After pasting your API key, click the "Save" button. Connect Loops API with Make ## Manage contacts and trigger workflows There are a number of actions available to manage contacts and send emails. Adding a module with Make ## Send transactional email There is a "Send transactional email" action you can use to send transactional emails. When setting up this action you can select an email from your Loops account, which will then display the required data variables in the form. Alternatively you can use the "Make an API call" action. Set up transactional sending in Make Make sure that the URL is `/v1/transactional` rather than the full endpoint URL. You can copy an email's example payload from its Publish page in Loops by clicking the **Show payload** button. Paste it into the "Body" field in Make and select the data you want to send to Loops. View the API reference for sending transactional email. ## Triggers The Make integration contains a trigger that lets you to listen for [Loops webhooks](/docs/webhooks), allowing you to do things in Make based on activity in your Loops account. All of Loops' webhook events are available as triggers in Make. Loops were renamed to Workflows on May 6, 2026. Make trigger bodies still use webhook `loop` names for compatibility, including `loop.email.sent`, `loopId`, `loopName`, and `sourceType: "loop"`. Add the **Watch Events** trigger to your Make scenario and use an existing or new Loops connection ([see above](#create-a-connection)). Make webhook settings You will be prompted to copy the webhook URL provided by Make and [paste it into your Loops webhook settings](/docs/webhooks#set-up-webhooks). Make sure to toggle on the **Immediately as data arrives** option in your scenario's bottom toolbar in Make so that the trigger fires every time a webhook event arrives. # Novu integration Source: https://loops.so/docs/integrations/novu Send Novu email notifications with Loops SMTP. Set up an SMTP connection to send Novu notification emails with Loops. ## Set up Loops SMTP in Novu Go to the [Integration Store settings](https://dashboard-v2.novu.co/integrations) in Novu and create a new email provider. Click **Connect Provider** and then **Custom SMTP**. Give your provider a name like "Loops SMTP" in the **Name** field. Then add the following details into each field: | Field | Value | | -------- | ------------------------------------------------------------------------------------------- | | User | `loops` | | Password | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops | | Host | `smtp.loops.so` | | Port | `587` | You also have to add values to the **From email address** and **Sender name** fields because Novu requires them. You can add any value here because Loops will overwrite these values when sending emails. Click **Create Integration** to finish setup. Setting up custom SMTP in Novu ## Create Transactional emails in Loops Next, create new transactional emails for the emails you are sending from Novu. In Loops, go to the [Transactional page](https://app.loops.so/transactional) and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates). Creating a transactional email template in Loops You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like. You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails. For each Loops template you create, you can add [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Novu to be inserted into each email. Once you're done creating the email and adding the data variables, click **Next**. On the next page, click the **Show payload** button to view the API payload for your template. You will need this for the next step. Viewing the transactional email payload in Loops Make sure to also publish your email! It won't send unless it's published. Read our detailed guide for sending transactional emails. ## Configure email templates in Novu The final setup step is to add email templates in Novu. Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data. In Novu, go to **Workflows** and create a new workflow. Give it a descriptive name and click **Create workflow**. You will enter the workflow UI. Add an **Email** node. In the email body paste the transactional payload from Loops from the previous step. Novu template editor Add a subject, but note that this will be overwritten by the subject you added to your Loops transactional email. Next you need to add some Novu data into the template, namely the recipient's email and any data variables you added in Loops. Here is an example **Confirm signup** email template. This payload was copied from the template's Publish page in Loops, then the `{{ subscriber.email }}` and `{{ payload.loginUrl }}` variables from Novu were added. You can add any custom data to the `payload` object when triggering emails from Novu. We need to pass those same values to your Loops transactional email via the `dataVariables` data. ```json Email template in Novu theme={"dark"} { "transactionalId": "cm67vfcgs00pha22s3qevs7nr", "email": "{{ subscriber.email }}", "dataVariables": { "loginUrl": "{{ payload.loginUrl }}" } } ``` If you want to add each Novu subscriber to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ subscriber.email }}` value. ```json Email template in Novu {4} theme={"dark"} { "transactionalId": "cm67vfcgs00pha22s3qevs7nr", "email": "{{ subscriber.email }}", "addToAudience": true, "dataVariables": { "loginUrl": "{{ payload.loginUrl }}" } } ``` If you want to include [Novu subscriber attributes](https://docs.novu.co/concepts/subscribers#subscriber-attributes) to personalize your Loops email, you can add them into your Novu template just like the following example. Make sure to add fallbacks for all non-required values and add the corresponding data variables (e.g. `firstName` here) into your Loops transactional email. ```json Email template in Novu {6} theme={"dark"} { "transactionalId": "cm67vfcgs00pha22s3qevs7nr", "email": "{{ subscriber.email }}", "dataVariables": { "loginUrl": "{{ payload.loginUrl }}", "firstName": "{{ subscriber.firstName | default: 'subscriber' }}" } } ``` Here's how a template with variables added looks in the Novu editor: Novu template editor Now you're all set up to start sending! ## Trigger emails with Novu When it comes to triggering Novu notifications to subscribers, you can use the Novu [SDKs](https://docs.novu.co/sdks/overview) or [API](https://docs.novu.co/api-reference/events/trigger-event). In each call, you need to specify your workflow by its ID, add recipient data and also pass in any data variables for your Loops transactional email into the payload. Using the same example from above, here's the trigger using Novu's Node SDK. You can add a `to.email` value to send notifications to a specific email address. ```javascript theme={"dark"} import { Novu } from "@novu/node"; const novu = new Novu(""); await novu.trigger("", { to: { subscriberId: "67867a14722783d44d69fc5a", }, payload: { loginUrl: "https://myapp.com/login/", }, }); ``` Here's the same request using the API: ```json theme={"dark"} POST https://api.novu.co/v1/events/trigger { "name": "", "to": { "subscriberId": "67867a14722783d44d69fc5a" }, "payload": { "loginUrl": "https://myapp.com/login/" } } ``` To view all sends of your transactional emails, click through to the email from the [Transactional](https://app.loops.so/transactional) page in Loops, where you'll find the Metrics page containing a table showing all sends and some statistics. ## Testing the integration Novu offers a testing UI where you can try out your set up before going live. Go to the **Workflows** page and click on your workflow, then select the **Trigger** tab. Here you will be able to set different subscriber and payload data and send test email notifications. The SDK examples below also update, so you can easily create code for your application. You can also see logs of all emails sent from the [Activity Feed](https://dashboard-v2.novu.co/env/_/activity-feed) page in Novu. Novu testing ## Important notes * The subject in Novu templates is always overwritten by the subject added to the corresponding template in Loops. * The **From email address** and **Sender name** configured in your Novu SMTP settings are always overwritten by the sender details added to your templates in Loops. # PostHog Source: https://loops.so/docs/integrations/posthog Send events and contacts data to Loops from PostHog. PostHog's Loops integration lets you send events from your project to Loops, as well as add users to your Loops audience. PostHog's [data pipelines](https://posthog.com/cdp) add-on or a self-hosted version of PostHog is required for this integration to work. Here's how to set up a Loops destination in PostHog. ## Create a Loops destination In your PostHog dashboard go to **Data pipelines**, click **+ New destination** and search for "Loops". You will see two options: * Send events to Loops.so (use this to trigger [workflows](/docs/workflows)) * Update contacts in Loops.so (this will *create or update* contacts in your Loops audience) Search for Loops in PostHog Choose the one you want and click **+ Create**. ## Add your API key You need to add a Loops API key to each destination so that PostHog can send data to your Loops account. In Loops, go to [Settings -> API](https://app.loops.so/settings?page=api) and generate or copy an API key. Back in PostHog, paste the API key in the **Loops API Key** field. Add Loops API key ## Customize the destination There are a few options available to customize the data that is sent to Loops for both destination types. This helps make sure that you're sending the correct data for your events and contacts. ### Send events In the **Send event to Loops.so** destination, you can filter which events are sent to Loops from the **Filters** section. The default is "Pageview" (which is sent with the event name "\$pageview" to Loops). Send events destination By default `pathname` is sent with the event as an [event property](/docs/events/properties), making it available in all emails triggered by the event. If you want to add more event properties you can use the **Property mapping** option. If you check the **Include all properties as attributes** option, all properties from the event will be sent to Loops. ### Update contacts The **Update contacts in Loops.so** destination lets you sync users to Loops. If a matching contact doesn't already exist in Loops, one will be created. In this destination you can filter which events are sent to Loops from the **Filters** section. The defaults are "Identify" and "Set person properties". Update contacts destination By default `email`, `userId`, `firstName` and `lastName` are synced to Loops. To add more contact properties you can use the **Property mapping** option, which allows you to map specific person data to contact properties in Loops. You can see all available contact properties in your account from [Settings -> API](https://app.loops.so/settings?page=api). Make sure to use the "API name" when mapping values in PostHog. If you check the **Include all properties as attributes** option, all properties from the person record in PostHog will be added to contacts in Loops. This may create a lot of custom contact properties in Loops, so we suggest running a mock test (see below) first to see which data would be sent. ### Mailing lists To add contacts to your Loops [mailing lists](/docs/contacts/mailing-lists), you need to make a quick code change to the destination. Click **Edit source code** on your destination's page. For the **updating contacts** destination, edit lines 6–9 and add a new `mailingLists` section. Here, add your mailing list ID(s) and `true` (to subscribe the contact) or `false` (to unsubscribe). ```javascript {4-6} theme={"dark"} let payload := { 'email': inputs.email, 'userId': person.id, 'mailingLists': { '': true } } ``` Read the API documentation for updating contacts in Loops. For the send events destination, edit lines 6–11 and add a new `mailingLists` section. Here, add your mailing list ID(s) and `true` (to subscribe the contact) or `false` (to unsubscribe). ```javascript {6-8} theme={"dark"} let payload := { 'email': inputs.email, 'userId': person.id, 'eventName': event.event, 'eventProperties': {}, 'mailingLists': { '': true } } ``` Read the API documentation for sending events to Loops. ## Testing To test if your configuration is working as expected, expand the **Testing** section at the bottom of the page. Testing the integration You can send real test events to Loops by clicking **Test function**. Check [Settings -> Events](https://app.loops.so/settings?page=events) and your [Audience](https://app.loops.so/audience) pages in Loops to see if data is coming across as you expect. If you'd rather not send actual data to Loops during testing, select the **Mock out HTTP requests** option. This will show the expected request data (including all contact and event properties) rather than send a request to Loops. # RudderStack Source: https://loops.so/docs/integrations/rudderstack Connect Loops to hundreds of apps to manage contacts and send emails. Our RudderStack integration lets you: * Create and update contacts * Send events to trigger workflows ## Configuring the destination In RudderStack, go to [Destinations](https://app.rudderstack.com/destinations) and click **New destination**. Search for "Loops" and select it. Adding Loops in RudderStack Select the source you want to connect Loops to. On the next page you need to add a Loops API key. You can generate a new one on the [Loops API Settings page](https://app.loops.so/settings?page=api) and paste it into the **API Key** field. Configuring the destination Click **Continue** at the bottom of the page to finish the setup. ## Create or update a contact To send contact data to Loops, use RudderStack's [`identify` call](https://www.rudderstack.com/docs/event-spec/standard-events/identify/). Identify users with a unique user ID from your source. You can include contact properties in the traits object, like `firstName` and `lastName` in this example. For new contacts, make sure to include an email address, otherwise the call will fail. If the contact has already been sent to Loops with its user ID, you can omit the email address unless you want to update that value. In general, we recommend to always include an email address. ```javascript theme={"dark"} rudderanalytics.identify(userId, { email: "test@example.com", firstName: "Adrian", lastName: "Brown" }); ``` To manage [mailing list](/docs/contacts/mailing-lists) subscriptions, add a `mailingLists` object to the traits object. The key is the ID of the mailing list and the value is a boolean indicating whether the contact should be subscribed or unsubscribed. ```javascript theme={"dark"} rudderanalytics.identify(userId, { email: "test@example.com", firstName: "Adrian", lastName: "Brown", mailingLists: { cly2xnjqn002z0mmn68uog1wk: true, }, }); ``` ## Send an event You can trigger emails from RudderStack by triggering [events](/docs/events) via [`track` calls](https://www.rudderstack.com/docs/event-spec/standard-events/track/). You should add and define your events in [Settings -> Events](https://app.loops.so/settings?page=events) including any expected [event properties](/docs/events/properties). The event name in your `track` call must match the name of the event in Loops. Data sent in the properties object will be sent as event properties to Loops, for use in your emails. Make sure to call `identify` before `track` so the event is associated with a specific contact. ```javascript theme={"dark"} rudderanalytics.track("newUser", { plan: "Pro Annual", accountType: "Facebook" }); ``` ## Testing ### In RudderStack RudderStack includes features to help you test your integration. First of all you can see all `identify` and `track` calls coming in from your sources. Click on a source and select the **Events tab**. Source events chart Click on the **Live events** button in the top right to view details of events as they come in. You can also see all of the calls being sent from RudderStack to your destinations. Click on a destination and then the **Events** tab. At the bottom of this page you will see a table showing events. Click on these to see any errors that have occurred when sending data to Loops. Destination events ### In Loops You can verify contact updates have happened in Loops from the [Audience page](https://app.loops.so/audience), and you can see all incoming events from the [Events page](https://app.loops.so/settings?page=events). Click on individual events in the **Event Log** table to view the payload that Loops processed. Events page # Segment Source: https://loops.so/docs/integrations/segment Utilize our official Segment integration to manage contacts and send email. Our Segment integration lets you: * Create and update contacts * Send events to trigger workflows Visit our [Segment integration](https://segment.com/catalog/integrations/actions-loops/) to learn more and follow the steps below. ## Configuring the destination After opening the link above, click **Configure Loops (Actions)**. Adding Loops in Segment Select your data source, give the destination a name, and click **Create destination**. Next, you’ll need an API key. You can generate a new one for Segment on the [Loops API Settings page](https://app.loops.so/settings?page=api). Enter the API key on your Segment destination settings: Add an API key Enable the destination and click **Save Changes**. Note that no data will start flowing until you create specific mappings for Loops. ## Mappings Segment action destinations require that you map specific fields from your source to your destination (in this case Loops). You can set this up by clicking into the **Mappings** tab and adding a new mapping. Currently we support updating contacts in Loops and sending events into Loops. ### Create or update contact First, select which events to map. Typically for contact creation and updates, the most useful event to map will be "Identify". Map contact properties The next step is to load a sample event to help you map fields appropriately. Contact properties are found in the `traits` object. For this example, we’ll be using this test event: ```json theme={"dark"} { "messageId": "segment-test-message-gt3ds8", "timestamp": "2023-05-24T17:58:30.352Z", "type": "identify", "email": "test@example.com", "traits": { "firstName": "Adam", "favoriteColor": "blue", "favoriteNumber": 42 }, "userId": "test-user-a5h7xb" } ``` When sending a contact's details to Loops, you must include an Email and a User ID. We've provided some defaults for the mappings, which will show up in the third step, but it is important you review them: Default contact mappings #### Custom contact properties Segment does not provide an interface to provide the names and types for [custom contact properties](/docs/contacts/properties#custom-contact-properties) that you might be using with Loops. In our example, those fields are `favoriteColor` and `favoriteNumber`. You can pass contact properties as a dictionary in the **Custom Contact Attributes** field. Ensure that the keys and values you provide match the schema you’ve created in your [Contact properties settings](https://app.loops.so/settings?page=api). Click **Edit Object** to specify the custom fields you want to send to Loops individually: Mapping custom contact properties #### Subscribed In most cases, you want to leave the **Subscribed** field as the default (deselected). Setting this to `true` will re-subscribe contacts who had previously unsubscribed, and setting it to `false` will unsubscribe contacts from receiving email. Leaving it deselected will default new users as subscribed to email and not update the email preference for existing contacts. Subscribed field #### Mailing lists To subscribe contacts to [mailing lists](/docs/contacts/mailing-lists) there are two options. The first method is to manually edit the **Mailing Lists** data. This will allow you to enter list ID values that are the same for every contact. Click the **Edit Object** option, then the **+ Add Mapping Field** button. In the **Select event variable** field enter "true" or "false" (to subscribe or unsubscribe) and in the **Enter key name** field enter your list ID(s). You can add multiple lists by clicking on the **+ Add Mapping Field** button again. Make sure the data shown below the fields has the same structure as in the image below. Adding list IDs in the mapping UI Another option is to add mailing list data to your Identify call, which will let you dd more dynamic data for each contact. You can test this by adding a `mailingLists` object to `traits` in your test event with list IDs as keys and `true` (to subscribe) or `false` (to unsubscribe) as values. ```json theme={"dark"} { "messageId": "segment-test-message-gt3ds8", "timestamp": "2023-05-24T17:58:30.352Z", "type": "identify", "email": "test@example.com", "traits": { "firstName": "Adam", "favoriteColor": "blue", "favoriteNumber": 42, "mailingLists": { "cm06f5v0e45nf0ml5754o9cix": true, "cm16k73gq014h0mmj5b6jdi9r": true, } }, "userId": "test-user-a5h7xb" } ``` Then you need to map the data. Click the **Select Object** option, then search for `traits.mailingLists` from the Event Variables options. Selecting the mailing list data from traits #### Testing After the mappings are configured you can preview the data that will be sent. Event data preview You can also send a test event to Loops to verify everything is working (this will send actual data to your Loops account). If it works, you will get a successful response: Successful response Check your [Loops audience page](https://app.loops.so/audience) to ensure the contact was created or updated as intended. Sending another test event with the same User ID or Email will update the existing contact instead of creating a new contact. ### Send event You can send events to trigger [workflows](/docs/workflows). First, select which event(s) to map. Typically for sending an event, the most useful event to map will be "Track". We recommend that you filter the events down to only ones that you plan on using within Loops using the **Event Name** filter: Sent event in Segment Then after defining or loading a sample event in step 2, configure the mapping. Sent event in Segment #### Required fields The **User ID** field is required. If a contact already exists in your Loops audience, the **Contact Email** field is optional; if the contact does not exist in Loops you need to include an email address so that a contact can be created and an email can be sent (we recommend always including an email address). #### Event properties If you want to add [event properties](/docs/events/properties), you can pass them as a dictionary to the **Event Properties** field. These properties can be included in emails triggered by your event. You can choose to select the `properties` object to send all data from the track call. Or if you click **Edit Object** you can select individual properties. Make sure to enter the correct contact property name from Loops in the **Enter key name** field. Configure event mappings #### Contact properties You can update contacts at the same time as sending an event with the **Contact Properties** field. Click **Edit Object** and specify the properties and values you want to update on your contacts. Mapping custom contact properties Ensure that the keys and values you provide match the schema you’ve created in your [Contact properties settings](https://app.loops.so/settings?page=api), otherwise some data may not be captured by Loops. You can choose to add a contact's email address in the **Contact Email** field or in the **Contact Properties** object. #### Testing After configuring the mapping, you can send a test event at the bottom of the page (this will send actual data to your Loops account). You can preview the data that will be sent. Send event preview The response should indicate success: Success message You can verify the event was received on your [Events page](https://app.loops.so/settings?page=events) in Loops. ## Sending data to Segment The following examples show how you can send data from your application to Segment for the two Loops actions. The examples use Segment's [Analytics.js library](https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/), but the premise is similar for other libraries. ### Create or update contact For this action you should send a `identify()` event. Add contact properties (including any [custom properties](/docs/contacts/properties#custom-contact-properties)) in the traits object: ```javascript theme={"dark"} analytics.identify("97980cfea0067", { email: "test@example.com", plan: "premium", logins: 5 }); ``` If you're mapping a `mailingLists` object from `traits` ([read more above](#mailing-lists)) add it like this: ```javascript theme={"dark"} analytics.identify("97980cfea0067", { email: "test@example.com", plan: "premium", logins: 5, mailingLists: { "cm06f5v0e45nf0ml5754o9cix": true, "cm16k73gq014h0mmj5b6jdi9r": true, } }); ``` ### Send event For this action send a `track()` event. Data sent in the properties object will be sent as [event properties](/docs/events/properties) to Loops. Make sure that you have added these event properties in your [Events Settings](https://app.loops.so/settings?page=events) in Loops before sending the event to Segment. ```javascript theme={"dark"} analytics.track("User Registered", { plan: "Pro Annual", accountType: "Facebook", firstName: "Phil" }); ``` You can update a contact's properties when sending events (for example, `firstName`) by mapping values in your properties within the **Contact Properties** object ([see above](#contact-properties)). # Stripe integration Source: https://loops.so/docs/integrations/stripe Sync contacts and send emails triggered by events in Stripe. Our Stripe integration lets you: * Create and update contacts * Send events to trigger workflows Our Stripe integration is built on top of [Incoming webhooks](/docs/integrations/incoming-webhooks) and runs via the Loops app for Stripe. You can sync your Stripe customer data to Loops for customer and invoice-related events and trigger emails with [workflows](/docs/workflows). ## Supported events We accept the following Stripe events: * `charge.dispute.created` * `charge.dispute.closed` * `checkout.session.async_payment_succeeded` * `checkout.session.async_payment_failed` * `checkout.session.completed` * `customer.created` * `customer.updated` * `customer.deleted` * `customer.subscription.created` * `customer.subscription.paused` * `customer.subscription.resumed` * `customer.subscription.trial_will_end` * `customer.subscription.deleted` * `invoice.paid` * `invoice.payment_failed` * `invoice.upcoming` * `quote.finalized` * `quote.accepted` * `quote.canceled` If you cannot see some of these events in the Stripe settings page, please install Loops's Stripe app. Click **Connect to Stripe** at the top of the page. [Stripe webhook docs](https://docs.stripe.com/webhooks) If you send other events, they will be ignored. If you would like to see more events supported, please let us know by sending a message to [our support team](https://app.loops.so/settings?page=support). Please keep in mind only events that contain an email address are able to be processed. ## Synced data We sync the following Stripe data to your Loops contacts for every incoming event: * Email address * First and last name (optional) We use the email addresses of Stripe customers to match contacts in your Loops audience. If the email address is not found in Loops, we will create a new contact. The `customer.deleted` event can be used to delete or unsubscribe your Stripe customers from your Loops audience. ## Install the Loops app in Stripe To get started, install the Loops Stripe app from [Settings -> Stripe](https://app.loops.so/settings?page=stripe). Click **Connect to Stripe** and follow the installation steps provided by Stripe. You will be redirected back to Loops when you are done. ## Configure events You can select and configure the Stripe events you want to be sent to Loops. For each event, you can choose to trigger workflows via [events](/docs/events) as well as update customer data like assigning a user group and syncing customer names. Make sure to click **Save** to save your changes. ## Testing Stripe webhooks You can test a `customer.*` webhook by creating a new customer in the [Customers](https://dashboard.stripe.com/customers) page in Stripe. You can also use the Stripe CLI tool to mimic events, by using the [`trigger` command](https://docs.stripe.com/cli/trigger). You can see all sent webhooks by going to **Developers -> Webhooks** and then clicking on an endpoint. On Loops' end, You will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page. ## Examples Here are some examples of how you can send data from Stripe to Loops to sync contacts and trigger useful emails to your customers. ### Syncing customers to Loops Create or update contacts in your Loops audience when a customer is created or updated in Stripe. 1. Install the Loops app in Stripe. 2. In Loops, make sure `customer.created` and `customer.updated` are toggled on on the Stripe settings page. 3. If you want to sync customer names, open the **Sync additional data** section and toggle on **Full name**. ### Send an email to all new Stripe customers Send an email from Loops when a new customer is created in Stripe. 1. Create a new workflow in Loops using our **Stripe - New Customer** template. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **customer.created** from the second dropdown. 3. Toggle on `customer.created` on the Stripe settings page. ### Send an email to Stripe Checkout customers Send an email from Loops when a customer pays via Stripe Checkout. 1. Create a new workflow in Loops using our **Stripe - Payment Successful** template. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **checkout.session.completed** from the second dropdown. 3. Toggle on `checkout.session.completed` on the Stripe settings page. ### Successful payment email Send an email from Loops when an invoice is paid in Stripe. 1. Create a new workflow in Loops using our **Stripe - Payment Successful** template. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **invoice.paid** from the second dropdown. 3. Toggle on `invoice.paid` on the Stripe settings page. ### Failed payment email Send an email from Loops when an invoice payment fails in Stripe. 1. Create a new workflow in Loops using our **Stripe - Payment Failed** template. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **invoice.payment\_failed** from the second dropdown. 3. Toggle on `invoice.payment_failed` on the Stripe settings page. ### Subscription created email Send an email from Loops when a new subscription is created in Stripe. 1. Create a new workflow in Loops. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **customer.subscription.created** from the second dropdown. 3. Add an email node to the workflow and create your email. 4. Toggle on `customer.subscription.created` on the Stripe settings page. ### Quote accepted email Send an email from Loops when a quote is accepted in Stripe. 1. Create a new workflow in Loops. 2. For the workflow trigger, select **Event received** and then select **Stripe** from the first dropdown and **quote.accepted** from the second dropdown. 3. Add an email node to the workflow and create your email. 4. Toggle on `quote.accepted` on the Stripe settings page. # Supabase integration Source: https://loops.so/docs/integrations/supabase Sync contacts and send emails triggered by events in Supabase. If you are looking to send Supabase authentication emails with Loops, check out our [Supabase SMTP docs](/docs/smtp/supabase). Our Supabase integration lets you: * Create and update contacts * Send events to trigger workflows Our Supabase integration is built on top of our [Incoming webhooks](/docs/integrations/incoming-webhooks) feature. This system lets you send webhooks from supported platforms directly to Loops so you can easily sync users and customers as well as send automated emails. [Please read our guide about incoming webhooks](/docs/integrations/incoming-webhooks) With Supabase, you can keep your user data synced to Loops so you can send emails to your userbase. ## Supported events We accept the following database events for the `auth.users` table: * `INSERT` * `UPDATE` * `DELETE` [Supabase database webhooks docs](https://supabase.com/docs/guides/database/webhooks) Events from other tables will be ignored. ## Synced data For `INSERT` and `UPDATE` events, we sync the following Supabase data to your Loops contacts: * User ID * Email address * First and last name (optional) We use the IDs of Supabase users to match contacts in your Loops audience. If the user ID is not found in Loops, we will create a new contact. First and last names are populated from metadata in the `auth.users` table. Key names should be `first_name` and `last_name`. ```javascript theme={"dark"} const { data, error } = await supabase.auth.signUp({ email: "test@example.com", password: "example-password", options: { data: { first_name: "John", last_name: "Doe", }, }, }); ``` More information about metadata in Supabase can be found in the [Supabase documentation](https://supabase.com/docs/guides/auth/managing-user-data?queryGroups=language\&language=js#adding-and-retrieving-user-metadata). `DELETE` events can be used to delete or unsubscribe your Supabase users from your Loops audience. ## Create a webhook endpoint in Loops [Follow the instructions here](/docs/integrations/incoming-webhooks#create-webhook-endpoints-in-loops) to create a new webhook endpoint, which will allow you to send webhook events directly to Loops. Endpoint form ## Create a database hook in Supabase Next, you need to set up database hooks in Supabase. Inside a project in Supabase, go to [Integrations -> Database Webhooks -> Webhooks](https://supabase.com/dashboard/project/_/integrations/webhooks/webhooks) and click **Create a new hook**. Give a name to your webhook, select "auth users" from the **Table** dropdown, and select the event(s) you want to get webhooks for. Make sure **HTTP Request** is selected in the **Webhook configuration** section. In the **HTTP Request** section, paste in the endpoint URL from Loops into the **URL** field. Adding a webhook in Supabase The last step is to secure requests with a header. In the **HTTP Headers** section, click **+ Add a new header**. From the **Secret header** section in Loops, paste in the **Header name** ("Loops-Secret") and **Header value** values into the form. Add header in Supabase Click **Create webhook** to finish. ## Testing Supabase webhooks You can test a user webhooks by creating, editing and deleting users from the [Authentication -> Users](https://supabase.com/dashboard/project/_/auth/users) page in Supabase, or by signing up in your application. On Loops' end, you will see new contacts appear in your [Audience](https://app.loops.so/audience) page, and triggered events in the [Events](https://app.loops.so/settings?page=events) page. # Webflow integration Source: https://loops.so/docs/integrations/webflow Enable signups from your site using a native Webflow form. This integration requires a paid Webflow plan to allow embedding custom scripts into your site. To allow sign ups to your audience from your Webflow site, you can utilize a native Webflow form plus some drop-in JavaScript. ## Add a custom form script to your Webflow site If you do not add the custom script in the correct place, the form may not work properly. To submit data to Loops seamlessly from your Webflow site we provide some JavaScript, which can be added to your site. Use this script in your Webflow page. ### Where to add the script * If you have a Loops form on every page of your site, add this code to the "Footer code" section in your Site settings ([read how in the Webflow docs](https://university.webflow.com/lesson/custom-code-in-the-head-and-body-tags#custom-code-in-site-settings)). * If you have a Loops form on only one page, add this code to the "Before \ tag" section in your Page setting ([read how in the Webflow docs](https://university.webflow.com/lesson/custom-code-in-the-head-and-body-tags#before-the-\<-body>-tag)). ## Add a form to your page Next you need to create a form in your Webflow page. Use the "Input" and "Button" elements. When you add new fields, make sure the “Name” value in the field's settings panel matches the name of the field in Loops: `email`, `firstName`, etc. You can check the full list of your available properties from your [API Settings](https://app.loops.so/settings?page=api) page. Please make sure these contact properties already exist in your Loops account. You can add new contact properties in [API Settings](https://app.loops.so/settings?page=api), with a [CSV import](/docs/add-users/csv-upload) or [using the API](/docs/api-reference/intro). Webflow form field settings showing the “Name” attribute ### How to add hidden fields You may want to assign a property to all contacts that submit the form (for example, `source` or `userGroup`). For this add an "Embed" component *inside your form* on the same level as your input and button elements. Webflow embed element inside a form for hidden fields In this Embed element add a hidden text field that passes on the custom value to Loops (make sure the `name` values match the "API Name" values in your [API Settings](https://app.loops.so/settings?page=api)). ```html theme={"dark"} ``` Mailing lists need to be marked **Public** in your [Lists settings](https://app.loops.so/settings?page=lists) in order for them to work in forms. Here's how it looks in the Webflow editor: Webflow editor showing hidden fields embedded in the form ## Add your Loops form endpoint URL The last step is to make sure your form submits data to Loops. You do this by adding a Loops form endpoint as the form's "Action" value. 1. Go to the [Forms page](https://app.loops.so/forms) in your Loops account. 2. Click on the **Settings** tab. 3. Copy the URL shown in the **Form Endpoint** field. Loops form endpoint URL in form settings 4. In Webflow, click on your Form Block, go to the Settings panel and paste the URL into the **Action** field. Webflow form settings showing the Action URL Our form submission endpoint has rate limiting, so you will see an error in testing if you submit more than once per minute or submit the same email twice. # Zapier integration Source: https://loops.so/docs/integrations/zapier Connect Loops to thousands of apps to manage contacts and send email. Our Zapier integration lets you: * Create, find and update contacts * Send events to trigger workflows * Send transactional email * Trigger Zaps based on activity in your Loops account Zapier lets you connect thousands of other platforms to Loops. We have created Zapier Actions for managing contacts, sending events and sending transactional emails and Zapier Triggers based on contact updates and email sending. ## Creating a new Zap To create a new Zap—for example, to connect Tally and Loops—you can either type out what you want to create. Create a new Zap Alternatively, click the **+ Create** button. Select Tally as the **Trigger** (using the "New Submission" event) and Loops as the **Action** (selecting "Add Contact" as the event). This would send new Tally submissions directly into Loops! A new Zap ### Authentication To be able to use Loops Actions, you need to connect to your Loops account. From the **Account** tab, click **Sign in**. Sign in to Loops Create or copy an API key from your Loops [API settings page](https://app.loops.so/settings?page=api) and paste it into the **API Key** field. Add API key into Zapier If you want to remove or edit your Zapier connections to Loops, go to [Apps -> Loops](https://zapier.com/app/connections/loops). You can create connections to multiple Loops accounts from a single Zapier account. ## Triggers There are a number of Loops triggers available in Zapier: * Contact created * Contact unsubscribed * Contact deleted * Contact subscribed to mailing list * Contact unsubscribed from mailing list * Campaign email sent * Loops email sent * Transactional email sent The "Email sent" triggers will fire for every individual email sent. If you send a campaign to 1,000 contacts, it will be triggered 1,000 times. Accompanying data is sent with each trigger. You can see the data available for each trigger by looking up the corresponding [webhook events](/docs/webhooks). Loops were renamed to Workflows on May 6, 2026. Zapier trigger bodies still use webhook `loop` names for compatibility, including `loop.email.sent`, `loopId`, `loopName`, and `sourceType: "loop"`. You can use the `contactIdentity` object to identify the contact that triggered each event. For example, the "Contact subscribed to mailing list" trigger will fire when a contact is subscribed to a mailing list and contains the following data: ```json theme={"dark"} { "eventName": "contact.mailingList.subscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "mailingList": { "id": "cm4ittp2k000l12j3lgrzvlxt", "name": "test mailing list", "description": null, "isPublic": true } } ``` ## Actions ### Add a contact This action adds new contacts to your Loops audience. If the email address already exists, it will return an error. All default [contact properties](/docs/contacts/properties) are available, plus [mailing lists](/docs/contacts/mailing-lists) and any custom contact properties added to your Loops account. Only the **Email** field is required. The **Source** field defaults to “Zapier” but you can update this to whatever value you like. Add a contact ### Update a contact The "Update a contact" action will create a contact if a matching contact does not already exist, making it useful if you don't know in advance if a contact exists in Loops. This action supports the same fields as "Add a contact". Only the **Email** field is required. Update a contact ### Find a contact This action supports searching your contact list for a specific email address. You also have the option to create a new user if one is not found. Find a contact ### Delete a contact This action will delete a contact by email address. Delete a contact ### Send an event This event will send an event in Loops. You need to specify an **Email** or **User ID** value to identify the contact, plus an **Event Name**. The action also supports [event properties](/docs/events/properties) and [mailing lists](/docs/contacts/mailing-lists). The **Email**/**User ID** and **Event Name** fields are required. Send an event ### Send a transactional email This action sends a transactional email and can optionally add a contact to your audience. The **Transactional Id**, **Email** and **Data Variables** fields are required. Send a transactional email # Quickstart Source: https://loops.so/docs/quickstart Get started with Loops: set up your sending domain, import contacts, create your first email, and trigger messages with events, API, or integrations. This is your guide to getting started with Loops. If you're new to setting up email for your SaaS company, this is the guide you should start with. ## What is Loops? Loops is an email platform, that helps you send marketing and transactional emails from our app, API and integrations. With Loops, you can track events and contact properties and then use that information to send emails to increase revenue, engagement or just generally improve your user's experience of your app. **Let's get started ✨** What we'll be covering... 1. [Set up your domain records](#1-set-up-your-domain-records) 2. [Import contacts](#2-import-contacts) 3. [Collect signups with a form](#3-collect-signups-with-a-form) 4. [Create your first email](#4-create-your-first-email) 5. [Send transactional email](#5-send-transactional-email) ## 1. Set up your domain records The first step is to set up your domain, so you can send emails through Loops. You need to do this before you can send any emails. We send from your domain so your emails appear as if they are coming from you. Set up your sending domain in Loops We choose to send from a `mail.loops.so` subdomain but you can send from your root domain if you'd prefer. To set up your domain in Loops, you need to add some MX, TXT and CNAME records to your domain's DNS settings so that we can verify that you own the domain you want to send emails from. Once you've set up your domain records, you'll be able to start sending emails! You can always send the records to a developer to help you integrate them. Read how to [add a member to your team](/docs/account/team-members). ## 2. Import contacts To send marketing and product emails to your contacts, you will need to import those contacts into Loops. Note: this isn't required if you only plan to use transactional email, as those contacts can be emailed directly via the API. If you have any existing contacts, i.e from a waitlist, your early access users, a database or your audience on a different platform, you can get started by importing them via CSV. You can import contacts via [CSV upload](/docs/add-users/csv-upload), [API](/docs/api-reference/intro) or through one of our [integrations](/docs/integrations). The most popular path is to import a CSV of existing contacts, then going forward automatically add contacts using our API, a form or an integration. ### Contacts Contacts are unique users in your Loops audience. We use email and a unique identifier to distinguish contacts. The only required field a contact must have is an email. Contacts table in Loops ### Contact properties Contact properties are additional pieces of information you can associate with a contact. They can include things like name, location, job title, and more. We provide [default properties](/docs/contacts/properties#default-contact-properties) like name, user group and source, but you are free to add any number of [custom properties](/docs/contacts/properties#custom-contact-properties) to your contacts, too. Contact properties in Loops You can use contact properties to [segment your audience](/docs/contacts/filters-segments) and send more targeted emails to specific groups. For example, you could send a promotional email only to contacts with a certain job title or to those in a specific user group. ## 3. Collect signups with a form Adding a form to your site is one (popular) way to automatically add new contacts to your Audience. Even if you're adding contacts programmatically via API or integration, in most cases you'll also want to have an input form on your page to collect emails for newsletters or product updates. To add a form to your site, head over to the [Forms page](https://app.loops.so/forms). You will see a handful of customization options including the form style, placeholder text, success message, font, font color, button color, and more. Make as many changes as you need to create a form that matches your brand. Simple form When you have finished customizing your form, simply copy the HTML or JSX that is automatically generated and paste it into your site. For more flexibility, you can create custom HTML forms that work with Loops. [Read our full guide about custom forms](/docs/forms/custom-form) or check out our [form-based integrations](/docs/integrations#manage-contacts). } href="/integrations/framer" /> } href="/integrations/webflow" /> ## 4. Create your first email To create your first email, first select the type of content you'll be sending. You can send email as a campaign, workflow or transactional email. You can also choose to start with a [Template](https://app.loops.so/templates) instead of starting from scratch. Choose an email type in Loops **Campaigns** are single marketing emails sent to a group (e.g. newsletters, product updates, announcements, investor updates), **Workflows** are automated emails sent based on specific triggers or conditions (e.g. onboarding sequences) and **Transactional** emails are one-off emails sent to a single person (e.g. forgot password, two-factor authorization codes, receipts). [Read more](/docs/types-of-emails) In this example, we will build a product update (a campaign), which could be sent to your users if you're building a SaaS. They should be sent monthly or at a faster cadence depending on shipping speed and contain a high-level overview of what you shipped over the last 30 days. To get started, click the **Create** button on the Home screen, followed by **Campaign**. Then we’re going to personalize by adding [dynamic content](/docs/creating-emails/personalizing-emails) and [style it](/docs/creating-emails/editor) to match our brand. Adding personalization Adding styling You can preview your email any time by hitting the paper airplane icon in the top right of the editor window. Once you're finished with the email content, click **Next** in the top right to choose your audience. Now we'll select the audience segment to whom we'll be sending the update. Since we're sending a product update, we want to send it to our entire audience so we won't be adding any audience filters. If you'd like to segment your audience, just click **Add filter**, which will open the filtering options. Filter campaign audience Click the **Next** button top right and you'll see options to send the email immediately or to schedule it for a time in the future. Schedule a campaign Click **Next** one last time to see a review of your email and settings (you can also see a preview of your email, too). If you're happy with how everything looks, click **Schedule send** on the last page and we're done! The email is now scheduled to go out. By the way, you can cancel the scheduled send at any time between the send time and now to update it, or you can just send it right away. ## 6. Set up an automated mail sequence We suggest that new Loops users warm up their new sending domain with a welcome email sequence. A slow ramp up of emails sent to highly-engaged recipients will help prepare your domain for larger campaigns later on ([read more](/docs/deliverability/sending-to-large-audience)). You can create an onboarding or welcome sequence using what we call workflows. A workflow looks like this: Example workflow in Loops Go to the [Workflows page](https://app.loops.so/loops) and click **New**. We'll start with the "Introduce yourself" template. This will create a workflow with a "Contact added" trigger (meaning every new contact will be added to the workflow), with an already-written introduction email ready for you. Edit the email and when you're ready to make it live, click **Start**. You can use [branches](/docs/workflows/branching) to create more complex workflows, sending contacts down different branches depending on contact properties or even whether they've interacted with campaigns you've sent from Loops. Branches in a workflow ## 7. Send transactional email You'll likely need to send a password reset, login or other automatic email that confirms a user action. These non-promotional emails are considered **Transactional emails** and are the 1:1 emails that are sent to a single contact via API or integration. They're included in all paid Loops plans, and also included within the 4,000 monthly sends available in the [Free plan](/docs/account/free-plan). To get started, click the **Create** button on the Home screen, followed by **Transactional**. Next, it’s time to write and style your email. We recommend following a similar style across all of your Transactional emails. You can do this by using [themes](/docs/creating-emails/styles#themes). Let’s create a Password Reset email together. Add copy and styling, and then to add dynamic content click the **Insert data variables** icon and specify a data variable name. These data variables will be populated with real content when you send the email using the API. Add data variables Click the **Next** button top right to view the data needed in your API call. View the payload Hit **Publish** to finalize the email. Copy the payload details and the ID; you'll need these to send the email using the API. Make an [API request](/docs/api-reference/send-transactional-email) to the transactional endpoint (or use an [SDK](/docs/sdks)). ``` POST https://app.loops.so/api/v1/transactional ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You will need the payload copied from before. Make sure to include values for all of the data variables you added to the email. ```json theme={"dark"} { "transactionalId": "clfq6dinn000yl70fgwwyp82l", "email": "test@example.com", "dataVariables": { "name": "Chris", "passwordResetLink": "https://example.com/reset-password" } } ``` To test sending transactional emails you can use an API tool like [Postman](https://www.postman.com), [Httpie](https://httpie.io) or [Insomnia](https://insomnia.rest) to make API requests. ## 8. Integrate with other platforms Loops integrates with thousands of other platforms, making it easy to send email to your audience, users or customers, regardless of where they originate. Create an action in Make Set up connections to different apps using a tool like [Zapier](/docs/integrations/zapier) or [Make](/docs/integrations/make), or create contacts using events from [Segment](/docs/integrations/segment). Our [webhook integration for Stripe](/docs/integrations/stripe) lets you easily sync customers and send automated emails based on payment events. ## Get support from the team Whether you’re sending your very first emails for your business or are switching over from another service, we’re always here to help! Every page of Loops has a small `?` widget in the bottom right-hand corner. Click it to receive instant support. 💬 Do you prefer live chat? Click and chat! 💌 Do you prefer email? [Send away](mailto:chris@loops.so) 🧑‍💻 Do you prefer a video call? [Book it](https://calendly.com/chris-loops/loops-in-app-support) # Loops SDKs Source: https://loops.so/docs/sdks Official and community SDKs to integrate Loops into your application. ## Official SDKs These SDKs are produced and maintained by the Loops team. 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.
Don't see your favorite language or framework? Request an SDK
## Unofficial SDKs The following SDKs are community-submitted and have not been officially reviewed or endorsed by Loops. We recommend thoroughly testing and reviewing the code before integrating it into your project. * [Elixir](https://github.com/abradburne/loops_ex) by Alan Bradburne * [Go](https://github.com/tilebox/loops-go) by Tilebox * [Java](https://github.com/telos-tech/loops-java-sdk) by Telos Technologies * [Laravel](https://github.com/plutolinks/laravel-loops) by PlutoLinks * [.NET](https://github.com/soenneker/soenneker.loops.openapiclient) by Jake Soenneker * [PHP](https://github.com/plutolinks/loops-php) by PlutoLinks * [Python](https://github.com/doctorgpt-corp/pyloops) by Ask Aletta * [Ruby on Rails](https://github.com/danielfriis/loops_rails) by Daniel Friis
Submit an SDK
## SMTP integrations You can also send transactional email using our [SMTP service](/docs/smtp). Check out our guides for popular frameworks. } title="Send with SMTP from Django" /> } title="Send with SMTP from Rails" /> ## Guides # Go SDK Source: https://loops.so/docs/sdks/go The official Loops SDK for Go. ## Installation Install the package with `go get`: ```bash theme={"dark"} go get github.com/loops-so/loops-go ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You will need a Loops API key to use the SDK. In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**. Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an environment variable). ## Usage ```go theme={"dark"} package main import ( "log" loops "github.com/loops-so/loops-go" ) func main() { client := loops.NewClient("YOUR_API_KEY") err := client.SendEvent(loops.SendEventRequest{ Email: "user@example.com", EventName: "signup", EventProperties: map[string]any{ "plan": "pro", }, }) if err != nil { log.Fatal(err) } } ``` API errors are returned as `*loops.APIError` with `StatusCode` and `Message`. See the [SDK README on GitHub](https://github.com/loops-so/loops-go) for error handling, retries, pagination, uploads, and the full method list. See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging). Browse types, methods, and package documentation. View the source code and release notes. Read the Loops API reference. # JavaScript SDK Source: https://loops.so/docs/sdks/javascript The official Loops SDK for JavaScript, with full TypeScript support. [![NPM downloads](https://img.shields.io/npm/dt/loops?style=social\&label=Downloads)](https://www.npmjs.com/package/loops) ## Installation Install the package [from npm](https://www.npmjs.com/package/loops): ```bash theme={"dark"} npm install loops ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. Minimum Node version required: 18.0.0. You will need a Loops API key to use the SDK. In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**. Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an `.env` file). } href="/sdks/javascript/nextjs" > Read our guide for sending email from Next.js projects. ## Usage ```javascript theme={"dark"} import { LoopsClient, APIError } from "loops"; const loops = new LoopsClient(process.env.LOOPS_API_KEY); try { const resp = await loops.createContact("test@example.com"); // resp.success and resp.id available when successful } catch (error) { if (error instanceof APIError) { console.log(error.json); console.log(error.statusCode); } } ``` Import `RateLimitExceededError` to handle rate limits. See the [SDK docs](hhttps://www.npmjs.com/package/loops) for every method, TypeScript types, rate limiting, and error handling. See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging). View the full documentation on npm. View the source code and release notes. Read the Loops API reference. # Nuxt module Source: https://loops.so/docs/sdks/nuxt The official Loops Nuxt module. [![NPM downloads](https://img.shields.io/npm/dw/nuxt-loops?style=social\&label=Downloads)](https://www.npmjs.com/package/nuxt-loops) This Nuxt module makes it easy to add the Loops [JavaScript SDK](/docs/sdks/javascript) to your Nuxt project. ## Installation You can install the package [from npm](https://www.npmjs.com/package/nuxt-loops): ```bash theme={"dark"} npm install nuxt-loops ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You will need a Loops API key to use the module. In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**. Copy this key and save it in your application code (for example as `LOOPS_API_KEY` in an `.env` file). Then add `nuxt-loops` to your modules list and add a reference to your API key: ```js nuxt.config.ts theme={"dark"} export default defineNuxtConfig({ modules: ["nuxt-loops"], loops: { apiKey: process.env.LOOPS_API_KEY, }, }); ``` ## Usage The Loops API and SDK should only be used on the server side to protect your API key. To use the module, import `loops` from the request context. Then call one of the SDK methods. Read through the [JS SDK docs](https://www.npmjs.com/package/loops) for more details. ```javascript theme={"dark"} export default defineEventHandler(async (event) => { const { loops } = event.context; const response = await loops.updateContact("test@example.com", { firstName: "Bri", lastName: "Chambers", }); }); ``` See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging). Explore our official JS/TS SDK. Read the Loops API reference. # PHP SDK Source: https://loops.so/docs/sdks/php The official Loops PHP package. [![Packagist Total Downloads](https://img.shields.io/packagist/dt/loops-so/loops?style=social)](https://packagist.org/packages/loops-so/loops) ## Installation Install the package [using Composer](https://packagist.org/packages/loops-so/loops): ```bash theme={"dark"} composer require loops-so/loops ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You will need a Loops API key to use the SDK. In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**. Copy this key and save it in your application code (for example in an environment variable). ## Usage ```php theme={"dark"} use Loops\LoopsClient; use Loops\Exceptions\APIError; $loops = new LoopsClient(env('LOOPS_API_KEY')); try { $result = $loops->contacts->create('test@example.com', [ 'firstName' => 'John', ]); } catch (APIError $e) { echo $e->getMessage(); $returnedJson = $e->getJson(); $statusCode = $e->getStatusCode(); } ``` Use `RateLimitExceededError` to handle rate limits. See the [SDK docs](https://packagist.org/packages/loops-so/loops) for every method, namespaces, and error handling. See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging). View the full documentation on Packagist. View the source code and release notes. Read the Loops API reference. # Ruby SDK Source: https://loops.so/docs/sdks/ruby The official Loops SDK for Ruby. [![Gem Total Downloads](https://img.shields.io/gem/dt/loops_sdk?style=social)](https://rubygems.org/gems/loops_sdk) ## Installation Add the gem to your Gemfile: ```bash theme={"dark"} bundle add loops_sdk ``` Or install it directly: ```bash theme={"dark"} gem install loops_sdk ``` You can install [Loops skills](/docs/skills) to help your coding agents use the Loops CLI, API and SDKs to create campaigns, manage contacts, send events, and send transactional emails. You will need a Loops API key to use the SDK. In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**. Copy this key and save it in your application code (for example in an environment variable). ## Usage Configure the SDK in an initializer: ```ruby config/initializers/loops.rb theme={"dark"} require "loops_sdk" LoopsSdk.configure do |config| config.api_key = ENV["LOOPS_API_KEY"] end ``` Then call SDK methods in your application: ```ruby theme={"dark"} begin response = LoopsSdk::Contacts.create( email: "test@example.com", properties: { firstName: "John" } ) rescue LoopsSdk::APIError => e puts "Loops API Error: #{e.json['message']} (Status: #{e.statusCode})" end ``` See the [SDK docs](https://github.com/Loops-so/loops-rb) for every method, rate limiting, and error handling. See the API documentation to learn more about [rate limiting](/docs/api-reference/intro#rate-limiting) and [error handling](/docs/api-reference/intro#debugging). View the gem on RubyGems. View the documentation and source code. Read the Loops API reference. # Security and Vulnerability Reporting Source: https://loops.so/docs/security-reporting How to report security or vulnerability issues. At Loops, we care deeply about the safety and security of our customers' data and our systems. We welcome security and vulnerability reports as part of our commitment to providing the most secure product possible. ## Making a report If you’ve read this document and discovered an issue that you believe is in-scope, please email us at `security@loops.so`. Please include the following details: * A clear summary of the issue and its potential impact. * Detailed steps to reproduce the issue. * Relevant environmental details (browser, OS, version numbers, etc.). * Any proof-of-concept code that demonstrates the vulnerability, if available. Our security team will review your report and keep you updated on our progress, requesting additional information or clarification when needed. We believe that vulnerability reporting creates a safer, better product for our customers. As such, we offer compensation for reports with a CVSS score of 4 or higher. ## Timelines We'll get back to you within a few days to acknowledge your report. ## What we’re most interested in * Authentication bypass and privilege escalation. * Exposure of personally identifiable information (PII). * Unauthenticated access to user data (outside of intentionally public data). ## In scope * [https://loops.so](https://loops.so) * [https://app.loops.so](https://app.loops.so) * [https://c.vialoops.com](https://c.vialoops.com) ## Out of scope * Automated scanning. * Social engineering. * Denial of Service attacks. * Attacks that need physical access to someone's computer. * Theoretical attacks you can't actually exploit. * Man-in-the-middle attacks. * Clickjacking or UI redress attacks. * CSV injection (unless it can harm non-Loops users). * HTML injection (unless it can harm non-Loops users). * Missing security headers, weak TLS cipher suites, or DNS setup issues. We might find these informative, but they probably won't earn a bounty. ## Please be considerate while investigating * Only test with your own account (or get permission from the account owner first). * Don't modify, delete, or store private data that isn't yours. * Avoid anything that might break or slow down our services. * If you get remote access to our systems, don't try to expand or elevate your access. ## Safe harbor Any activities conducted in a manner consistent with this document will be considered authorized and Loops will not initiate legal action against you. # Setting up your domain Source: https://loops.so/docs/sending-domain Steps for adding a sending domain to your account. When you set up your account for the first time, you need to set up your domain records in order to start sending email. We'll be sending email on your behalf, so we need to verify that you own the domain you're sending from. Here's how to set it up in just a few steps. You need to use your own domain to send with Loops. Free subdomains provided by hosting platforms (e.g. `yourapp.vercel.app`, `yoursite.netlify.app`, `yourshop.myshopify.com`) cannot be used as sending domains, because you don't have access to configure the required DNS records. ## Step 1: Add your sending domain During the sign up flow, you are asked to specify your desired sending domain. You can enter any domain here that you can set up DNS records for, and if you choose a subdomain, it doesn't need to exist yet. We [recommend using a subdomain for this](/docs/deliverability/sending-from-subdomain), e.g. `mail.yourcompany.com`, rather than sending from your root domain `yourcompany.com`. Adding a domain during sign up If you have already signed up and want to edit your sending domain, you can do so from **Settings -> Domain**. You do not need to create this subdomain with an A or CNAME record. Loops will provide all DNS records you need to set up. ## Step 2: Set up your records From the **Settings -> Domain** page, click **View records** (or [click this link](https://app.loops.so/sending-domain) to go directly). Domain DNS records page in Loops On this page, you'll see a few things. **Your records** These are SPF, DKIM and MX records that need to be set up in your domain zone editor inside of your domain registrar like Namecheap, Google Domains, AWS, Godaddy or elsewhere. Next to each record is a clipboard icon. You can use this to copy the records to your clipboard and easily paste them into your domain registrar. **Your sending domain** This is indicated below by “yourcompany.com”. This will have your domain listed. If you'd like to change domains, you can do so in the [account settings](https://app.loops.so/settings). **A verify records button** Once you have copied your records to your registrar, click this button to verify they have been set up correctly. Verify DNS records button in Loops ## Step 3: Add your domain records Copy and paste the records one by one into your registrar. You want to use the **Type** (indicated as TXT, CNAME and MX) in setting up your records, ***not*** the title of the record e.g. SPF, DKIM, MX. Loops' records for SPF are at envelope.sendingdomain.com, meaning they won't collide with any other SPF records you have set up. We specify a DMARC record so that you have one, but you can also just have a single DMARC at the root domain level. Want inbox providers to display your verified logo? Set up BIMI (Brand Indicators for Message Identification) after your domain authentication is in place. Learn more in our [What is BIMI? guide](/docs/guides/what-is-bimi). DNS records can be added from the "DNS" page within a website. Click **Add record** to open the form. Select a "Type" (TXT, MX or CNAME), then paste the "Name" and "Value" information. [Read the guide](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/#create-dns-records) Be sure to set the proxy to “DNS Only” for the CNAME records: Cloudflare DNS record form Dreamhost is currently unsupported in full because you cannot add custom MX records. For GoDaddy, [read this guide](https://www.godaddy.com/help/manage-dns-records-680). Google Domains (and potentially other providers) combine the mail server and priority inputs into a single line. So if you receive an error like this when setting up the domain, make sure to instead type out the input like this: `10 feedback-smtp.us-east-1.amazonses.com` Google Domains DNS record editor Google domains will also include quotes “ “ around some record names. This is expected and will not impact anything. Go to the **Advanced DNS** page for your domain. If you are using the automatic Gmail/Gsuite integration with Namecheap, you will need to disable the automatic integration and switch to **Custom MX** in the **Mail Settings** dropdown. You then need to [add an MX record](https://support.google.com/a/answer/174125#current\&legacy\&zippy=%2Cgoogle-workspace-current-version-later) to set up Gmail on your domain again. Then you can add Loops' MX records by clicking **Add new record** in the "Mail Settings" section and pasting in the values provided in Loops. Click the `✓` icon to save each record. Namecheap mail settings DNS records Add the TXT and CNAME records by clicking **Add new record** in the "Host Records" section. Namecheap host records DNS settings For Route 53, [read this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-creating.html). For adding your records to Squarespace, [read this guide](https://support.squarespace.com/hc/en-us/articles/31120985010957-DNS-records-for-email#toc-option-2---add-custom-records-manually). Note that you might need to trim a trailing period from the record values. Unfortunately, Wix DNS [does not support subdomains](https://support.wix.com/en/article/request-connecting-a-mailbox-to-a-subdomain) for MX records when your nameservers are pointed at Wix. If you purchased a domain outside of Wix, you should use the ["Pointing" method](https://support.wix.com/en/article/connecting-a-domain-to-wix-using-the-pointing-method) for your domain, which will let you set up DNS records externally a domain registrar. Then you can add records using [this guide](https://support.wix.com/en/article/managing-dns-records-in-your-wix-account). Make sure you enter the “Priority” while setting up your MX record. In most registrars this is done by formatting it like “10 pastedrecordname”. Occasionally you will be asked to place it on a separate line. Just make sure to read the instructions on the page as you set up your MX record and if you have any questions, [contact our support team](https://app.loops.so/settings?page=support). ## Step 4: Verify your records are set up correctly After you have copied and pasted your records into your domain registrar, click **Verify Records** at the bottom of the page to check your configuration is correct. Sometimes records can take up to an hour to propagate across all the servers. During that time you may see different records validate. This is totally normal, just check back later. If the domain is set up correctly, you should see a page like the one below. If not, check back soon; sometimes records can take some time to propagate. Notice the "Records present" in green next to each record section. Domain verification success in Loops ## Domain already in use If you're getting a "domain already in use" error when trying to set up your domain, this typically means someone on your team has already registered an account with Loops using your domain. Here are the steps you can take to resolve this: Go to the [Loops login page](https://app.loops.so/) and request a login link using your email address. Search for `loopsbot@mail.loops.so` in your email inbox(es) to see if you have received other registration or login emails from Loops. Ask your teammates if someone has already set up a Loops account for your organization. If so, ask them to invite you to the existing account. If you've tried all these steps and still can't access your domain, [contact our support team](https://app.loops.so/settings?page=support) for manual assistance. Confused or have questions? Just reach out to [support](https://app.loops.so/settings?page=support). # Sending your first email Source: https://loops.so/docs/sending-first-email A guide for creating and sending emails with Loops, including setup steps and sending best practices. So you're ready to send your first email from Loops! Let's go through some best practices and then see how creating an email works. ## Best practices Here are some important things to know and bear in mind when sending email with Loops. * We have a “low-html” editor, which means your emails send with a minimal amount of styles applied. We do this so your emails are highly readable and so they're more likely to not be placed in the spam folder or deprioritized in the inbox by your email provider. * Try not to use sensational copy like “sale”, “discount” or exclamation points in your emails. * It's also important to keep your emails short and to the point. Use an efficient subject line that encourages the reader to open the email and get to the point quickly in the body of the message. * Use personalization when possible to make the message more engaging and relevant to the reader by [personalizing your emails](/docs/creating-emails/personalizing-emails). ## Send your first email First, choose which type of email you want to send: a campaign, a workflow or a transactional email. [Find out about the types of email](/docs/types-of-emails) you can send from Loops. To send your first email, simply choose a template or start an email from scratch. Choose a template to start an email ## Sending settings Along with the email subject, you can determine some of the sending settings, like the "From" sending address. By clicking `>` you will reveal a [settings panel](/docs/creating-emails/sending-settings), where you can specify the From email address (which is always tied to your sending domain), Reply to email and the Preview text (typically shown in email clients just beneath the Subject). For workflow and transactional emails you can also specify a [CC and BCC address](/docs/creating-emails/sending-settings#cc-and-bcc). Email sending settings You can include dynamic variables in these fields, too, making them personalized for each recipient of your campaigns, workflows and transactional emails. Just click the contact property, event property or data variable icon next to each field. Adding dynamic data to the sending settings [Read more about sending settings](/docs/creating-emails/sending-settings) ## Add contact data You may want to add contact data into your emails, for example, a first name or a purchased product's name. You can do this by adding dynamic content to your email. [Learn more about personalizing emails](/docs/creating-emails/personalizing-emails) Insert a contact property into an email ## Make your message visual When you design an email with Loops, you can add instructive screenshots, GIFs, or images to create an engaging email. Simply drag and drop any image (including GIFs!) into the editor. You can also easily add links, lists, buttons and dividers to create more engaging and useful emails. We also offer a styling panel to customize design elements like font size, text color, background color, borders and spacing. Email editor with images and styling [Read more about our editor](/docs/creating-emails/editor) ## Preview your email Once you are happy with the design of your email, you can preview it in your email client. Click the **Send a preview** icon in the top right corner of the email editor to send a test email to yourself or anyone on your team from the "Send a Preview Email" modal. Send a preview email modal Preview email modal showing contact selection The **Add preview tag** toggle will add a "\[Preview]" to the start of the subject line to easily identify them in your inbox. Toggle this off to see how the email will appear in your recipients' inboxes. ### Dynamic content in previews When sending preview emails, Loops uses the contact properties and any declared fallbacks of the selected contacts in the preview modal. However, you can override this data in your preview email using the form in the modal. ### Test without sending emails You can test email sending by using email addresses with `@example.com` and `@test.com` domains. This will not actually send emails but is a great way to test workflows, schedule campaigns, or test transactional emails without affecting your sending domain's reputation. ## Set up your email sending for the first time Now you're ready to send the email! You are able to select which contacts to send to, then either send it now or schedule it for later. Wondering what the next step is to get started implementing emails for your SaaS? Check out the [getting started guide](/docs/quickstart). **Did we miss something?** Not to worry! Just [contact our support team](https://app.loops.so/settings?page=support). # Agent Skills Source: https://loops.so/docs/skills Make your agent work with Loops using skills. Skills are small modules that teach your coding agent how to work with Loops. You can install them with a single command, then your agent will be able to reference them when working with Loops. Skills work in tools like Claude, Codex, Cursor, and ChatGPT. ## API skill This skill allows your agent to build with the Loops API to manage campaigns, contacts, custom properties, mailing lists, events, and transactional emails. Best used inside an application, like a Next.js or Nuxt.js project. Install with: ```bash theme={"dark"} npx skills add loops-so/skills --global --skill loops-api ``` ## CLI skill This skill allows your agent to use the Loops CLI to manage contacts, mailing lists, send events, and send transactional emails. Best used inside an LLM. Install with: ```bash theme={"dark"} npx skills add loops-so/skills --global --skill loops-cli ``` ## LMX skill This skill allows your agent to use Loops' markup language (LMX) to create and update emails. Best used inside an LLM. Install with: ```bash theme={"dark"} npx skills add loops-so/skills --global --skill loops-lmx ``` ## Email best practices skill This skill helps your agent audit deliverability, lifecycle coverage, consent handling, and transactional vs marketing email setup. Best used inside a project where you want help improving your email sending. Install with: ```bash theme={"dark"} npx skills add loops-so/skills --global --skill loops-email-sending-best-practices ``` ## Links View the Loops skills repo on GitHub. View the Loops CLI repo on GitHub. # Send with SMTP Source: https://loops.so/docs/smtp Send Loops emails over SMTP. You can send transactional emails over SMTP, meaning you can use Loops to power emails in platforms like Supabase and development tools and frameworks like Laravel, Rails, Django and Nodemailer. ## How it works The Loops way of sending emails over SMTP is a bit different from other services. First, you create transactional email templates in [the Loops editor](/docs/creating-emails/editor) for the emails you want to send. Our rich editor helps you create beautiful, client-compatible *and* easy-to-update emails rather than hand-coding them. Use these SMTP settings in your application: | Field | Value | | ----------- | ------------------------------------------------------------------------------------------- | | Host | `smtp.loops.so` | | Port number | `587` | | Username | `loops` | | Password | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops | Then, when it comes to sending emails, instead of the content of an email, you send an API-like request body like this: ```json theme={"dark"} { "transactionalId": "clomzp89u035xl50px7wrl0ri", "email": "test@example.com", "dataVariables": { "confirmationUrl": "https://myapp.com/confirm/12345/" } } ``` This content **needs to be converted to a string** and then sent as the email body. Loops takes the provided data and compiles an email using the template you specify in the `transactionalId` value plus the provided `dataVariables`, then sends the email to `email`. Every email sent using Loops' SMTP service requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload. ## SMTP Integrations Learn how to set up SMTP in platforms and developer tools. ### Integrations } href="/integrations/auth0" > Send Auth0 authentication emails with Loops. } href="/integrations/novu" > Send Novu notification emails with Loops. Send Supabase authentication emails with Loops. ### Frameworks } href="/smtp/django"> Send transactional emails from your Django project. Send transactional emails from your Laravel project. } href="/smtp/rails"> Send transactional emails from your Rails project. # Django SMTP Source: https://loops.so/docs/smtp/django Send transactional emails from your Django project using Loops' SMTP service. As Loops' SMTP service requires sending an API-like email body rather than a full email, it's not recommended to use Loops as the default SMTP service for your app in your settings file.\ Instead, use a custom `connection` for each email request that you want to send through Loops. Sending email from Django with Loops' SMTP service is easy but there's one gotcha: the email body needs to be an [API-like payload](/docs/smtp#how-it-works). This may seem strange at first but it allows you to use Loops' WYSIWYG editor to craft your emails and keep email templating outside of your code repo. We are using a custom `connection` for sending this email as typically only some emails in a project will be sent through Loops. Add these settings to your project (e.g. in an `.env` file). | Field | Value | | ----------- | ------------------------------------------------------------------------------------------- | | Host | `smtp.loops.so` | | Port number | `587` | | Username | `loops` | | Password | An API key copied from your [API settings](https://app.loops.so/settings?page=api) in Loops | Every email sent from Django over Loops SMTP requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload. ```python theme={"dark"} from django.core.mail import send_mail, get_connection import json import os with get_connection( host=os.environ['LOOPS_SMTP_HOST'], port=os.environ['LOOPS_SMTP_PORT'], username=os.environ['LOOPS_SMTP_USER'], password=os.environ['LOOPS_SMTP_PASSWORD'], use_tls=True # Has to be True ) as connection: email = 'test@example.com' # This payload can be copied from a transactional email's # Publish page in Loops payload = { "transactionalId": "clomzp89u635xl30px7wrl0ri", "email": email, "dataVariables": { "buttonUrl": "https://myapp.com/login/", "userName": "Bob" } } send_mail( "Subject here", # Overwritten by Loops template json.dumps(payload), # Stringify the payload "test@example.com", # Overwritten by Loops template [email], fail_silently=False, connection=connection ) ``` # Laravel Source: https://loops.so/docs/smtp/laravel Send transactional emails from your Laravel project using Loops' SMTP service. Transactional emails with Loops simplifies your code. With our WYSIWYG editor and API-like payloads, you can design and manage email templates outside of your codebase, ensuring cleaner code and easier template maintenance. Unlike older SMTP services, Loops requires the body of emails sent via SMTP to be formatted as an [API-like payload](/docs/smtp#how-it-works). This approach allows you to use Loops' [powerful email editor](/docs/creating-emails/editor) to craft your emails and keep email templating outside of your application code. Every email sent over Loops SMTP requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload. Here's how you can set up transactional emails with Loops SMTP in Laravel: Create transactional emails in Loops using the [editor](/docs/creating-emails/editor). Add [data variables](/docs/transactional#add-data-variables) to your emails for any dynamic content you want to send from your Laravel application. Add a data variable To configure Loops SMTP in your Laravel project, add the following values to your `.env` file. `MAIL_PASSWORD` should be an API key from your [API Settings](https://app.loops.so/settings?page=api) page. ```bash .env theme={"dark"} MAIL_MAILER=smtp MAIL_HOST=smtp.loops.so MAIL_PORT=587 MAIL_USERNAME=loops MAIL_PASSWORD= MAIL_ENCRYPTION=tls ``` Now you can send emails from your application. If you haven't already, create a mailable class, for example `AuthEmail`: ```bash theme={"dark"} php artisan make:mail AuthEmail ``` Loops' SMTP system doesn't send full HTML emails directly. Instead, you should provide a structured API-like payload, which Loops will then use to render an HTML email. Create a view for your email, like below. You can copy an example payload from the **Publish** page of your transactional email in Loops. ```json resources/views/mail/auth-email-text.blade.php theme={"dark"} { "transactionalId": "clomzp89u635xl30px7wrl0ri", "email": "{{ $email }}", /* recipient */ "dataVariables": { "loginUrl": "https://myapp.com/login?code={{ $auth_code }}" } } ``` Then add a reference to your template in the `Content` definition using the `text` key. You also need to pass the values for the recipient email address and any data variables in your email. In this case we are using a `$user` property added to the constructor. ```php app/Mail/AuthEmail.php theme={"dark"} use App\Models\User; class AuthEmail extends Mailable { /** * Create a new message instance. */ public function __construct( private User $user, ) {} /** * Get the message content definition. */ public function content(): Content { return new Content( text: 'mail.auth-email-text', with: [ 'email' => $this->user->email, 'auth_code' => $this->user->auth_code, ] ); } } ``` You can omit the `view` option typically required for HTML emails in Laravel. Loops handles HTML rendering using the provided payload. You can skip adding values to the `Envelope` because the "from" address and subject are all defined within Loops on your transactional email. Now you can send transactional emails. ```php theme={"dark"} use \App\Mail\AuthEmail; Mail::to('test@example.com')->send(new AuthEmail($user)); ``` Note that the email address defined in `to()` will not be used for sending the email even though it's a required parameter. **You have to provide the recipient's email to the template itself**. You can read more about sending emails from Laravel [in their docs](https://laravel.com/docs/11.x/mail#writing-mailables). Read how to send transactional email with our API. Learn how to send transactional email with Loops. # Ruby on Rails Source: https://loops.so/docs/smtp/rails Send transactional emails from your Rails project using Loops' SMTP service. Transactional emails with Loops simplifies your code. With our WYSIWYG editor and API-like payloads, you can design and manage email templates outside of your codebase, ensuring cleaner code and easier template maintenance. Unlike older SMTP services, Loops requires the body of emails sent via SMTP to be formatted as an [API-like payload](/docs/smtp#how-it-works). This approach allows you to use Loops' [powerful email editor](/docs/creating-emails/editor) to craft your emails and keep email templating outside of your application code. Every email sent over Loops SMTP requires a transactional email to be set up in your Loops account. Note the `transactionalId` value in the email payload. Here's how you can set up transactional emails with Loops SMTP in Rails: Create transactional emails in Loops using the [editor](/docs/creating-emails/editor). Add [data variables](/docs/transactional#add-data-variables) to your emails for any dynamic content you want to send from your Rails application. Add a data variable To use Loops SMTP, configure Action Mailer using the following settings. The `password` value should be an API key from your [API Settings](https://app.loops.so/settings?page=api) page. ```ruby config/environments/production.rb theme={"dark"} config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.loops.so', port: 587, user_name: 'loops', password: '', authentication: 'plain', enable_starttls: true } ``` Now you can send emails from your application. Here's an example Mailer: ```ruby app/mailers/user_mailer.rb theme={"dark"} class UserMailer < ApplicationMailer def login_email @user = params[:user] # Note: the "to" address is required by Action Mailer # but is overwritten by the email provided in the view # (see below). Likewise, a subject is not required here # because Loops will use the subject provided in the editor. mail(to: @user.email) end end ``` Loops' SMTP system doesn't send full HTML emails directly. Instead, you should provide a structured API-like payload, which Loops will then use to render an HTML email. Create a text view for your email, like below. You can copy an example payload from the **Publish** page of your transactional email in Loops. ```json app/views/user_mailer/login_email.text.erb theme={"dark"} { "transactionalId": "clomzp89u635xl30px7wrl0ri", "email": "<%= @user.email %>", /* recipient */ "dataVariables": { "loginUrl": "https://myapp.com/login?code=<%= @user.auth_code %>" } } ``` You do not need to provide an HTML view for your emails when using Loops SMTP (i.e.: `login_email.html.erb` is not required). Read how to send transactional email with our API. Learn how to send transactional email with Loops. # Supabase SMTP Source: https://loops.so/docs/smtp/supabase Configure your Supabase account to send authentication emails with Loops. If you are looking to sync Supabase user data to Loops, you can use our [Supabase integration](/docs/integrations/supabase). Set up an SMTP connection to send all of your Supabase emails with Loops. There are two big benefits to using Loops for sending your Supabase emails: You can use [Loops' design editor](/docs/creating-emails/editor) to create (and then easily edit) beautiful transactional emails instead of having to code them with HTML. You get full visibility on which emails are being sent, when, and to whom in your Loops account. Supabase doesn't offer this view. ## Set up Loops SMTP in Supabase You can set up Loops SMTP in Supabase with just a few clicks. Go to [Settings -> Supabase](https://app.loops.so/settings?page=supabase) and click **Connect Supabase** in the **SMTP** section. Supabase SMTP settings When prompted by Supabase, select your organization and authorize access. Back in Loops, select your Supabase project and an API key (which is used as the SMTP password), then click **Set up SMTP**. This will add Loops SMTP credentials to your Supabase project, so you can start sending emails. You can find these credentials in Supabase at [Authentication -> Emails -> SMTP Settings](https://supabase.com/dashboard/project/_/auth/smtp). For reference, the SMTP credentials are: | Field | Value | | :---------- | :----------------------------------------------------------------------------------- | | Host | `smtp.loops.so` | | Port number | `587` | | Username | `loops` | | Password | An API key from your [API settings](https://app.loops.so/settings?page=api) in Loops | ## Create Transactional emails in Loops Next, create new transactional emails for the emails listed in Supabase ([Authentication -> Emails](https://supabase.com/dashboard/project/_/auth/templates)). You need to create both **Confirm signup** and **Magic Link** emails to be able to properly set up the integration. * Confirm signup (required) * Invite user * Magic Link (required) * Change Email Address * Reset Password Note that if a Supabase user has not previously confirmed their email, they will be sent a **Confirm signup** email when you request a **Magic Link** email. To create new transactional emails, go to the [Transactional page](https://app.loops.so/transactional) in Loops and click **New**. Alternatively, you can select one of our many ready-made templates from the [Templates page](https://app.loops.so/templates). Supabase template in the editor You can then use [the Loops editor](/docs/creating-emails/editor) to create nicely-designed templates or make them as simple as you like. You can even create [themes](/docs/creating-emails/styles#themes) to apply consistent design and branding to all of your emails. Saved styles For each Loops template you create, you need to [add data variables](/docs/creating-emails/personalizing-emails#add-dynamic-content-to-emails), which allow data from Supabase to be inserted into each email. For example, you could add a `confirmationUrl` data variable that you can map to the `{{ .ConfirmationURL }}` value from Supabase. You can also build URLs by including values like `{{ .SiteUrl }}` or add in a confirmation code using `{{ .Token }}`. Supabase values Once you're done creating the email and adding data variables, click **Review** in the left panel. Here you can see the API payload for your template. You will need this for the next step. Email payload Make sure to also publish your email! It won't send unless it's published. Read our detailed guide for sending transactional emails. ## Configure email templates in Supabase The final step is to make sure your emails in Supabase are configured to send the correct data to Loops. Loops SMTP integrations work a bit differently than most. Instead of sending a text or HTML email body, you set them up to send API-like data. In Supabase, go to [Authentication -> Emails](https://supabase.com/dashboard/project/_/auth/templates), then edit each template to contain the payload as shown in the previous step (you can click the clipboard icon in Loops to copy the full payload). Once pasted into the Message body, you need to add the Supabase message variables into the payload. Make sure you set up at least the **Confirm signup** and **Magic Link** templates in Supabase, otherwise emails will not be sent. Also, any variables added in the **Confirm signup** template need to also be available in **Magic link** email, because Supabase will send a **Confirm signup** email instead of a **Magic Link** email if a user hasn't confirmed their email address. Here is an example **Confirm signup** email template. This payload was copied from the template's Publish page in Loops, then the `{{ .Email }}` and `{{ .ConfirmationURL }}` Supabase variables were added. ```json theme={"dark"} { "transactionalId": "clvmzp39u035tl50pw7wrl0ri", "email": "{{ .Email }}", "dataVariables": { "confirmationUrl": "{{ .ConfirmationURL }}" } } ``` If you want to add each Supabase user to your Loops audience so you can send marketing email to them, add the `addToAudience` flag to your template as below. This will create a contact in Loops using the `{{ .Email }}` value. ```json theme={"dark"} { "transactionalId": "clvmzp39u035tl50pw7wrl0ri", "email": "{{ .Email }}", "addToAudience": true, "dataVariables": { "confirmationUrl": "{{ .ConfirmationURL }}" } } ``` Here's how the template looks in the Supabase editor: Supabase editor With the integration now all set up, your Supabase authentication emails will be sent via Loops, giving you more visibility on your email sends and the great addition of being able to build beautiful and easy-to-update emails in the Loops editor. To view all sends of your transactional emails, click through to the email from the [Transactional](https://app.loops.so/transactional) page in Loops, where you'll find the Metrics page containing a table showing all sends and some statistics. ## Testing the integration You can test your Loops integration by creating new users from [Authentication -> Users](https://supabase.com/dashboard/project/_/auth/users) page in Supabase. Click **Add user** and select **Create new user**. Use an email address you have access to. Then click on the new user in the list and click the **Send magic link** button. This will send the magic link email you set up in Supabase via Loops SMTP. ## Debugging Supabase offers a detailed view of authentication logs where you can look for issues with your Loops integration. Go to [Logs -> Auth](https://supabase.com/dashboard/project/_/logs/auth-logs). To narrow down your search you can look for logs like "magiclink" or "invite". Supabase logs ## Important notes * You need to add a template in Loops and set up the email in Supabase for at least the **Confirm signup** and **Magic Link** templates. * The subject in Supabase templates is always overwritten by the subject added to the corresponding template in Loops. * The sender name and sender email configured in your Supabase SMTP settings are always overwritten by the sender details added to your templates in Loops. * Any Supabase email not set up with the correct API-like payload will fail to send. # Transactional email Source: https://loops.so/docs/transactional Learn how to send, test, and troubleshoot transactional email in Loops. Transactional emails are automated, API-triggered emails that are sent to individual contacts based on a specific action they have taken. Examples include **confirmation emails**, **password reset emails**, and **purchase confirmations**. Read how to send transactional email with our API. ## How it works Sending transactional email with Loops has two steps. First, create transactional emails in Loops using our [email editor](/docs/creating-emails/editor). In your email you can add data variables, which let you insert custom data into each email you send. To send transactional emails you need to use the Loops API. All it takes is a simple call to our [Send transactional email endpoint](/docs/api-reference/send-transactional-email). Your request needs to include the ID of the transactional email you created, the recipient's email address and any data variables needed for the email. ## Transactional vs marketing emails Transactional emails are one-to-one messages triggered by user actions (like password resets, receipts, or login links). Marketing emails (campaigns and workflows) are promotional or lifecycle emails sent to your Audience. Key differences: * Transactional emails do not include an unsubscribe link. * Transactional emails do not track opens or link clicks. * Unsubscribed contacts can still receive transactional emails. You should not send marketing content as transactional email through Loops. [Read more about the differences between transactional and marketing emails](/docs/guides/transactional-vs-marketing-email). ## Contacts Transactional emails can be sent to any email address, whether or not the recipient exists in your Audience. * Your Audience only contains marketing contacts. If a new contact is sent a transactional email, they are not added to your Audience. * Sending a transactional email to a new contact will not trigger the ["Contact added" workflow trigger](/docs/workflows#triggers). * The "Subscribed" [contact property](/docs/contacts/properties#subscribed) does not affect transactional emails. Unsubscribed contacts will still receive all transactional emails they are sent. If you want to add a recipient to your Audience when sending a transactional email, set `"addToAudience": true` in your API request ([see below](#send-your-email)). ## Compose your email There are two ways to create emails in Loops: using our editor or import an MJML template. ### Use our editor You can [create emails in the editor](/docs/creating-emails/editor), letting you easily add formatting, images and buttons. Example transactional email in Loops You can add dynamic data into your emails by using data variables (like `name` in the example above). Read on for more information about data variables. ### Bring your own MJML You can also [upload your own MJML code](/docs/creating-emails/uploading-custom-email) to use in the email. This is useful if you have a pre-existing template you want to use. Like emails created in our editor, you can add data variables to MJML templates. To add a data variable called `passwordResetLink`, you can use it in your MJML like this: ```html theme={"dark"} {DATA_VARIABLE:passwordResetLink} ``` Note the uppercase “DATA\_VARIABLE” and the colon before the variable name. ## Add data variables Data variables let you insert dynamic values into every transactional email, to personalize them to each recipient. When it comes to sending the email, you specify the value of each data variable in the API call. Let's say you have a password reset email that is sent once a user clicks a "Reset Password" button in your application. For this we need to add two data variables to the email: `name` and `resetUrl`. You can insert data variables into the text of the email in three ways: * click the `{}` dynamic content button above the editor (see below) * type variables directly into the editor, like `{planName}` * use the `/` slash menu (select "Data variable" from the menu). Insert data variable button in the editor You can also add data variables as links (on text, images and buttons). To add the `resetUrl` as the button's link, click on the data variable icon (1), click the data variable (2) and then enter your data variable name into the **Variable name** field (3). Set a link to a data variable If you want to write the tags manually in your content or [in MJML emails](/docs/creating-emails/uploading-custom-email), you can use our [dynamic tag syntax](/docs/creating-emails/personalizing-emails#dynamic-tag-syntax). To add a reset URL you can write a tag like this: ``` {DATA_VARIABLE:resetUrl} ``` ### Data variables in email headers Data variables are also available in the email sending settings fields, like **From**, **Reply**, **CC**, **BCC** and **Subject**. Click the `>` button to view all fields and then click the data variable icon. Use sensible names for your data variables, like `bccAddress` or `replyToAddress` so they are clear in your API call. To send data to these fields, simply include the data variables in your API call as normal ([see below](#send-your-email)). Adding a data variable in the Subject field ### Optional data variables Data variables can be optional, by selecting "Optional" from the selector just below the data variable name. If a data variable is optional, you can omit the key from the API request entirely, or include the key and send an empty string `""` (sending `null` will not work). Optional data variables ### Array data variables [Arrays](/docs/creating-emails/editor#arrays) are a way to add repeatable items to an email (only available in transactional emails for now). Previewing an array block Once you add an array block to your email, you can edit its data variable name in the editor panel in the **Array** section (the default is `items`). Any data variables you add inside the array block will be repeated for each item in the array. Then, when [sending the email](#send-your-email), you can include an array of objects containing those variables. Adding data variables to an array block Read more about adding dynamic content into your emails. Read how to send transactional email with the API. ### Important information about data variables * Make sure that when you [send a transactional email using the API](/docs/api-reference/send-transactional-email) that you include *all non-optional data variables* in your request. If you do not include the correct set of data variables in your API call, the send will fail. * Data variable names are case-sensitive (meaning `LastLoggedIn` and `lastLoggedIn` are different variables). * Data variable names can only contain: * letters * numbers * underscores * dashes * Data variable values sent over the API can be `string` or `number`. ## Review your email On the next page, after clicking **Next**, you'll see the API Details section. This contains the data variables used in the email as well as a sample payload for reference. The “Transactional ID” lets you distinguish between different transactional emails when calling the API and is required. API details section before publishing ## Publish the email To enable sending the email, it needs to be published by clicking **Publish**. ## Send your email In the example above, two Data Variables were used in the email and there is a transactional ID needed as part of our API call. Any data variables created in the email are required when making the API request. Here's an example of the request for sending this email: **Send a POST to this endpoint (make sure to authenticate)** ``` https://app.loops.so/api/v1/transactional ``` **Payload** You can copy an email's example payload from its Publish page in Loops by clicking the **Show payload** button. ```json theme={"dark"} { "transactionalId": "clfq6dinn000yl70fgwwyp82l", "email": "test@example.com", "dataVariables": { "name": "Chris", "passwordResetLink": "https://example.com/reset-password", "bccAddress": "test@example.com" } } ``` You can add contacts to your audience from this call by adding `"addToAudience": true` to your payload. If you have added an [array block](/docs/transactional#array-data-variables) to your email, make sure to include the parent data variable that is tied to your array, then include an array of objects. ```json theme={"dark"} { "transactionalId": "clfq6dinn000yl70fgwwyp82l", "email": "test@example.com", "dataVariables": { "items": [ { "name": "Item 1", "description": "Description of Item 1" }, { "name": "Item 2", "description": "Description of Item 2" } ] } } ``` Read how to send transactional email with our API. Integrate our SDK into your JS or TS application. ## Editing the email To edit the email, click **Edit Draft** on the Compose page. Edit draft button for a transactional email The previous published version of the email will remain, and will continue sending until you republish the email. This means you can make changes to transactional emails without disrupting ongoing email sending. When you have a draft, we retain both versions and you can switch between your draft and the published version using the toggles in the top right. Toggle between draft and published versions To publish your changes, simply click **Republish** and click the confirmation. Your draft will seamlessly become the published version. ## Metrics After emails are sent, details are shown in the email's Metrics page. These include send time and if messages experienced any issues with delivery (bounces or spam complaints). Note that open and click tracking is disabled for transactional messages to improve deliverability for infrastructure-level communications. ## Testing transactional emails You can test your transactional email integration by sending to email addresses with `@example.com` and `@test.com` domains (for example `user@example.com` and `user@test.com`). Everything will work as normal (e.g. you will receive success responses from the API), but no emails will be sent to `@example.com` or `@test.com` email addresses, making this a good way to test transactional emails without affecting your sending domain’s reputation. ## Errors ### Links look like `x-webdoc://....` This is a known issue with Apple Mail. Make sure that your links start with `https://` or `http://` and they should work fine. ### API (400-level error) The first place to start is to check the body of the response. It will contain a JSON object with a `message` property that will give you more information about the error. Here are common reasons that the API might return with a 400-level error: * Using the API without a [verified domain](/docs/sending-domain). * Trying to use the API for a transactional email without a published email message. * Missing a required parameter: `transactionalId`, `email`, or `dataVariables`. * Missing a data variable that is required by the email message. # Attachments Source: https://loops.so/docs/transactional/attachments How to send attachments with your transactional email. Need to send attachments with your transactional email? Just [reach out to us](https://app.loops.so/settings?page=support) and we can enable the feature on your account. ## Sending attachments Check out [the transactional API documentation](/docs/api-reference/send-transactional-email) for a refresher on the transactional API email payload. To attach a file to a transactional message, you'll need to add a `attachments` key to the standard transaction API email payload. The `attachments` key should be an array of objects, each with the following keys: * `filename` - the name of the file * `contentType` - the MIME type of the file * `data` - the base64 encoded content of the file Here's an example of a transactional email payload with [this ICS file](https://gist.github.com/phil-loops/c0cb5d84d502a3949651934252d306af) attached: ```json theme={"dark"} { "transactionalId": "*********", "email": "phil+attachments@loops.so", "attachments": [ { "filename": "oil-change-invite.ics", "contentType": "text/calendar", "data": "QkVHSU46VkNBTEVOREFSClZFUlNJT046Mi4wClBST0RJRDotLy9pY2FsLm1hcnVkb3QuY29tLy9pQ2FsIEV2ZW50IE1ha2VyCkNBTFNDQUxFOkdSRUdPUklBTgpCRUdJTjpWVElNRVpPTkUKVFpJRDpBbWVyaWNhL0xvc19BbmdlbGVzCkxBU1QtTU9ESUZJRUQ6MjAyMzA0MDdUMDUwNzUwWgpUWlVSTDpodHRwczovL3d3dy50enVybC5vcmcvem9uZWluZm8tb3V0bG9vay9BbWVyaWNhL0xvc19BbmdlbGVzClgtTElDLUxPQ0FUSU9OOkFtZXJpY2EvTG9zX0FuZ2VsZXMKQkVHSU46REFZTElHSFQKVFpOQU1FOlBEVApUWk9GRlNFVEZST006LTA4MDAKVFpPRkZTRVRUTzotMDcwMApEVFNUQVJUOjE5NzAwMzA4VDAyMDAwMApSUlVMRTpGUkVRPVlFQVJMWTtCWU1PTlRIPTM7QllEQVk9MlNVCkVORDpEQVlMSUdIVApCRUdJTjpTVEFOREFSRApUWk5BTUU6UFNUClRaT0ZGU0VURlJPTTotMDcwMApUWk9GRlNFVFRPOi0wODAwCkRUU1RBUlQ6MTk3MDExMDFUMDIwMDAwClJSVUxFOkZSRVE9WUVBUkxZO0JZTU9OVEg9MTE7QllEQVk9MVNVCkVORDpTVEFOREFSRApFTkQ6VlRJTUVaT05FCkJFR0lOOlZFVkVOVApEVFNUQU1QOjIwMjMwOTExVDE4NTY0M1oKVUlEOjE2OTQ0NTg1ODM0MzgtOTkyMjJAaWNhbC5tYXJ1ZG90LmNvbQpEVFNUQVJUO1RaSUQ9QW1lcmljYS9Mb3NfQW5nZWxlczoyMDI1MDUwMVQxMjAwMDAKRFRFTkQ7VFpJRD1BbWVyaWNhL0xvc19BbmdlbGVzOjIwMjUwNTAxVDEyMDAwMApTVU1NQVJZOkNoYW5nZSBPaWwKVVJMOmd1bWRyb3AuZXhhbXBsZS5jb20KREVTQ1JJUFRJT046WW91ciBjYXIgaXMgcmVhZHkgZm9yIGFuIG9pbCBjaGFuZ2UhCkxPQ0FUSU9OOkd1bWRyb3AgVmlsbGFnZQpFTkQ6VkVWRU5UCkVORDpWQ0FMRU5EQVI=" } ] } ``` When you send this payload, the recipient will receive an email with the ICS file attached: An email with an ICS file attached All the usual [transactional API email payload](/docs/api-reference/send-transactional-email) keys are still required. The `attachments` key is in addition to the standard payload. ## Limitations * The total size of the JSON request body must be less than 4 MB. Keep in mind that attachments are base64 encoded, which increases file size by \~33%. * Attachments are not generally available. Please [contact us](https://app.loops.so/settings?page=support) if you need this feature enabled on your account. # Types of emails Source: https://loops.so/docs/types-of-emails Learn about the three types of emails that you can send with Loops: Campaigns, Workflows, and Transactional. Choose campaign, workflow, or transactional email ## Campaigns A Campaign is the right type of email for a one-off send to your audience or a segment of your audience. Marketing emails are a 1-to-many communication, meaning that the same exact email that you craft can (and probably will) be sent and read by a number of recipients or customers. **Examples:** * Newsletters * Investor updates * Product updates * User feedback requests **How to send a Campaign:** 1. Hit “Create” in the top right corner of your Loops dashboard. 2. Select “Campaign” in the popup module. 3. Enter the subject line, preview text, and content of the email. 4. Select your audience segment. 5. Schedule the email to send later or send it immediately. ## Workflows A Workflow is an email sequence that is triggered by an event, a contact being added to your audience, or a contact property update. **Examples:** * Welcome emails * User onboarding sequences * User check-ins For more information on sending your first workflow, [visit this guide](/docs/workflows). ## Transactional A Transactional email is an automated message that is triggered by a specific contact action. **Examples:** * Password resets * Purchase or upgrade confirmations * Shipping information * Account cancellation emails For more information on sending your first Transactional messages, [read the transactional email guide](/docs/transactional). **Things to note:** * Unlike campaigns and workflows, transactional emails are not promotional in nature and as a result, they do not require unsubscribe information to be included in the email. * Unlike campaigns and workflows, we do not track opens or link clicks in transactional emails, to increase deliverability of your emails. This also means that `email.opened` and `email.clicked` [webhook events](/docs/webhooks) are not available for transactional emails. * Contacts behave slightly differently between transactional and marketing emails (campaigns and workflows): * Your Audience only contains marketing contacts. If a new contact is sent a transactional email, they are not added to your Audience, unless you use the `addToAudience` flag when [sending the email](/docs/api-reference/send-transactional-email). * Sending a transactional email to a new contact will not trigger the ["Contact added" workflow trigger](/docs/workflows#triggers). * The "Subscribed" [contact property](/docs/contacts/properties#subscribed) does not affect transactional emails. Unsubscribed contacts will still receive all transactional emails they are sent. Are you stuck and wondering exactly how you should be setting up your email flows within Loops? [Contact support](https://app.loops.so/settings?page=support) and we will help get you set up. # Webhooks Source: https://loops.so/docs/webhooks Learn about receiving event notifications with webhooks, including setup steps and payload examples. Webhooks send data to your website or application when certain events happen in your Loops account. ## Set up webhooks Go to [Settings -> Webhooks](https://app.loops.so/settings?page=webhooks) and input the URL of your endpoint that will receive events. You will be provided with a signing secret. You should save this in your project (for example in an environment variable) so you can verify requests when you receive them. Currently you can only set up one webhook endpoint per Loops account. Subscribe to the events you want to receive using the toggles. Click the group names to view all events in each. Webhooks settings When toggling your endpoint on and off there may be a small delay before this setting is reflected on the server. For example, it may take a few seconds after toggling on your endpoint for requests to be dispatched. ## Rate limiting Webhook events will be sent at a maximum rate of 10 per second. Any further events will be queued. ## Verify requests Every event is signed so you can check that data sent to your endpoint is sent from Loops. To verify webhooks, you need to create a signature of the received request and match that to the provided signature in the request's headers. Here's an example verification function you could use in Next.js: ```javascript utils.ts theme={"dark"} import { NextRequest } from 'next/server'; import crypto from 'crypto'; interface WebhookVerificationError extends Error { code: 'MISSING_HEADERS' | 'MISSING_SECRET' | 'INVALID_SIGNATURE' | 'VERIFICATION_FAILED'; } /\*\* - Verifies a webhook request from Loops - @param req The incoming Next.js request - @throws {WebhookVerificationError} If verification fails - @returns {Promise} True if verification succeeds \*/ async function verifyWebhook(req: NextRequest): Promise { try { // Get the webhook-related headers directly from req.headers // Next.js automatically lowercases header names const eventId = req.headers.get('webhook-id'); const timestamp = req.headers.get('webhook-timestamp'); const webhookSignature = req.headers.get('webhook-signature'); // Verify required headers are present if (!eventId || !timestamp || !webhookSignature) { const error = new Error('Missing required webhook header') as WebhookVerificationError; error.code = 'MISSING_HEADERS'; throw error; } // Read raw body as buffer const readable = req.read(); const buffer = Buffer.from(readable); const rawBodyText = buffer.toString(); const signedContent = `${eventId}.${timestamp}.${rawBodyText}`; // Verify secret exists const secret = process.env.LOOPS_SIGNING_SECRET; if (!secret) { const error = new Error( 'Missing LOOPS_SIGNING_SECRET environment variable' ) as WebhookVerificationError; error.code = 'MISSING_SECRET'; throw error; } // Create a signature from the request data const secretBytes = Buffer.from(secret.split('_')[1], 'base64'); const signature = crypto .createHmac('sha256', secretBytes) .update(signedContent) .digest('base64'); // Check if the signature matches const signatureFound = webhookSignature .split(' ') .some((sig) => sig.includes(`,${signature}`)); if (!signatureFound) { const error = new Error('Invalid signature') as WebhookVerificationError; error.code = 'INVALID_SIGNATURE'; throw error; } return true; } catch (error) { if ((error as WebhookVerificationError).code) { throw error; } const wrappedError = new Error( `Webhook verification failed: ${(error as Error).message}` ) as WebhookVerificationError; wrappedError.code = 'VERIFICATION_FAILED'; throw wrappedError; } } ``` ```javascript webhook.ts theme={"dark"} import { NextApiRequest, NextApiResponse } from 'next'; export const config = { api: { bodyParser: false, }, }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } try { await verifyWebhook(req); // Webhook is verified, process the event... res.status(200).json({ message: 'Webhook processed successfully' }); } catch (error) { console.error('Webhook error:', error); const status = { MISSING_HEADERS: 400, MISSING_SECRET: 500, INVALID_SIGNATURE: 401, VERIFICATION_FAILED: 400 }[(error as WebhookVerificationError).code] || 500; return NextResponse.json( { message: error.message }, { status } ); } } ``` ## Testing webhooks From the Webhooks settings page you can send a test request to your endpoint. This allows you to test that your endpoint is working, and that your verification code is OK. The event name sent during testing is `testing.testEvent`. You can [see the payload below](#testing-testevent). ## Viewing webhook history Once Loops has started sending webhook events to your endpoint you will be able to see event history in the **Messages** section at the bottom of the Webhooks settings page. Clicking on an event in the table will reveal the response from your endpoint, which is helpful if there have been any errors. We retain 30 days of event history. Viewing webhook event history ## Event data Every webhook will contain the following data in the request body: Loops were renamed to Workflows on May 6, 2026. Webhook payloads still use `loop` names for compatibility, including `loop.email.sent`, `loopId`, `loopName`, and `sourceType: "loop"`. ### `eventName` The event type. See a [full list of events below](#event-types). ### `webhookSchemaVersion` Will be `1.0.0` for all events. ### `eventTime` Unix timestamp of the time the event occurred in Loops. *** Depending on the context of the event, more data will also be included. Full examples are shown in the [Event types](#event-types) section below. ### `contact` A full contact object containing a contact's properties. Contains: * `id` * `email` * `firstName` (nullable string) * `lastName` (nullable string) * `source` * `subscribed` (boolean) * `userGroup` * `userId` (nullable string) * `mailingLists` (object with mailing list IDs as keys and `true` as the value; these are the mailing lists the contact is subscribed to) * `optInStatus` (nullable string, `"accepted"` or `null`) This will be `null` for contacts unless they are created via a form and [double opt-in](/docs/contacts/double-opt-in) is enabled. `contact.created` events are only sent once contacts have confirmed their subscription, so this value will never be `"pending"` or `"rejected"`. * plus any custom contact properties This object is the same as the data returned in the [Find a contact](/docs/api-reference/find-contact#response) API endpoint. ### `contactIdentity` A contact's identifiers. To retrieve the full contact, use the [Find a contact](/docs/api-reference/find-contact) API endpoint. Contains: * `id` * `email` * `userId` (nullable string) ### `email` Details about an individual email send to a recipient: * `id` - The unique ID of the email. * `emailMessageId` - The ID of the sent version of the campaign, workflow or transactional email. * `subject` - The subject of the sent version of the campaign, workflow or transactional email. To get the ID of the campaign, workflow or transactional email that relates to the Loops dashboard or API, look for a `campaignId`, `loopId`, or `transactionalId` in the payload. ### `mailingList` Details about a mailing list: * `id` * `name` * `description` (nullable string) * `isPublic` (boolean) This object is the same as the data returned in the [List mailing lists](/docs/api-reference/list-mailing-lists#response) API endpoint. ### `mailingLists` A list of `mailingList` objects (see above), when an event relates to multiple mailing lists. ### `sourceType` For `email.*` events, this specifies the type of email. One of `campaign`, `loop` or `transactional`. Workflow emails use `loop`. *** ### Headers Headers will include: * `Webhook-Signature` - A list of request signatures, which can be used to [verify the request](#verify-requests). * `Webhook-Id` - The unique ID of the event. You can use this to check if you have already saved or processed this specific event. * `Webhook-Timestamp` - The timestamp of the request (seconds since epoch). ## Event types ### Contacts #### contact.created Sent when a new contact is created in your audience. Contains a `contactIdentity` object plus a full `contact` object, which includes all of the new contact's properties. When [double opt-in](/docs/contacts/double-opt-in) is enabled, contact webhooks don't fire until the contact is confirmed. The `contact.created` event will only be sent for contacts created via forms once the contact has confirmed their subscription. Other contact-related webhooks (such as `contact.mailingList.subscribed`) will also only fire after confirmation. ```json theme={"dark"} { "eventName": "contact.created", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4itta800003ow9hhekzk94o", "email": "test+5@loops.so", "userId": null }, "contact": { "id": "cm4itta800003ow9hhekzk94o", "email": "test+5@loops.so", "firstName": null, "lastName": null, "source": "API", "subscribed": true, "userGroup": "", "userId": null, "mailingLists": { "cm4ittp2k000l12j3lgrzvlxt": true }, "optInStatus": "accepted", "favoriteColor": "blue", "favoriteNumber": 42 } } ``` #### contact.unsubscribed Sent when * a contact is unsubscribed from your audience. * a contact is deleted from your audience (alongside [contact.deleted](#contact-deleted)). This is not the same as a contact unsubscribing from a mailing list. See [contact.mailingList.unsubscribed](#contact-mailingList-unsubscribed)). Contains a `contactIdentity` object. ```json theme={"dark"} { "eventName": "contact.unsubscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### contact.deleted Sent when a contact is deleted from your audience. Contains a `contactIdentity` object. ```json theme={"dark"} { "eventName": "contact.deleted", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### contact.mailingList.subscribed Sent when a contact is subscribed to a mailing list. Contains `contactIdentity` and `mailingList` objects. ```json theme={"dark"} { "eventName": "contact.mailingList.subscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "mailingList": { "id": "cm4ittp2k000l12j3lgrzvlxt", "name": "test mailing list", "description": null, "isPublic": true } } ``` #### contact.mailingList.unsubscribed Sent when a contact is unsubscribed from a mailing list. This is not the same as a contact unsubscribing from your audience. See [contact.unsubscribed](#contact-unsubscribed). Contains `contactIdentity` and `mailingList` objects. ```json theme={"dark"} { "eventName": "contact.mailingList.unsubscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "mailingList": { "id": "cm4ittp2k000l12j3lgrzvlxt", "name": "test mailing list", "description": null, "isPublic": true } } ``` ### Email sending #### campaign.email.sent Sent when a campaign is sent to a contact. This event will fire for every campaign send. If you send a campaign to 1,000 contacts, you will receive 1,000 events. Contains `campaignId` and `campaignName` values plus `contactIdentity` and `email` objects. If the campaign was sent to one or more mailing lists, a `mailingLists` list will also be included. ```json theme={"dark"} { "eventName": "campaign.email.sent", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "campaignId": "cm4t1suns001uw6atri87v54s", "campaignName": "Test Campaign", "email": { "id": "cm4t1sv84004yje79hawr1fi1", "emailMessageId": "cm4t1suns001ww6atotin3bn1", "subject": "Test Subject" }, "mailingLists": [ { "id": "cm4ittp2k000l12j3lgrzvlxt", "name": "test mailing list", "description": null, "isPublic": true } ] } ``` #### loop.email.sent Sent when a workflow email is sent to a contact. This event keeps its pre-May 6, 2026 `loop` name for compatibility. This event will fire for every contact in a workflow. If 1,000 contacts get sent emails from your workflow, you will receive 1,000 events. Contains a `loopId` and `loopName` values plus `contactIdentity` and `email` objects. If the workflow was sent to one or more mailing lists, a `mailingLists` list will also be included. ```json theme={"dark"} { "eventName": "loop.email.sent", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "loopId": "cm4t1snfj0052icemfshgqfcw", "loopName": "Test Loop", "email": { "id": "cm4t1socj004mje79e61mgh7d", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "mailingLists": [ { "id": "cm4ittp2k000l12j3lgrzvlxt", "name": "test mailing list", "description": null, "isPublic": true } ] } ``` #### transactional.email.sent Sent when a transactional email is sent. Contains a `transactionalId` value plus `contactIdentity` and `email` objects. ```json theme={"dark"} { "eventName": "transactional.email.sent", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null }, "transactionalId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" } } ``` ### Email events #### email.delivered Sent when an email is delivered to its recipient. Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects. ```json theme={"dark"} { "eventName": "email.delivered", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.softBounced Sent when an email soft bounces. Soft bounces are temporary email delivery failures, for example a connection timing out. Soft bounces are retried multiple times and some times the email is delivered. Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects. ```json theme={"dark"} { "eventName": "email.softBounced", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.hardBounced Sent when an email hard bounces. Hard bounces are persistent email delivery failures, for example a mailbox that doesn't exist. The email will not be delivered. In Loops, a hard bounce results in a contact being unsubscribed from your audience so a `contact.unsubscribed` event will also be sent. Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects. ```json theme={"dark"} { "eventName": "email.hardBounced", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "9874cm4t1sseg004tje7982991nan8732843", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.opened Sent when a campaign or workflow email is opened. Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects. This event is not available for transactional emails because email opens are [not tracked](/docs/transactional#tracking) for transactional emails. ```json theme={"dark"} { "eventName": "email.opened", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.clicked Sent when a link in a campaign or workflow email is clicked. Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects. This event is not available for transactional emails because link clicks are [not tracked](/docs/transactional#tracking) in transactional emails. ```json theme={"dark"} { "eventName": "email.clicked", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.unsubscribed Sent when a recipient unsubscribes from marketing email or a mailing list using an email's "Unsubscribe" link. A `contact.unsubscribed` or `contact.mailingList.unsubscribed` event will also be sent depending on whether the email was sent to a mailing list or not. Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects. This event is not available for transactional emails because unsubscribe links are [not included or required](/docs/types-of-emails#transactional) for transactional emails. ```json theme={"dark"} { "eventName": "email.unsubscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.resubscribed Sent when a recipient resubscribes to marketing email from an email's preference center ("Unsubscribe" link). Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects. This event is not available for transactional emails because unsubscribe links are [not included or required](/docs/types-of-emails#transactional) for transactional emails. ```json theme={"dark"} { "eventName": "email.resubscribed", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` #### email.spamReported Sent when a recipient reports your email as spam. Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects. ```json theme={"dark"} { "eventName": "email.spamReported", "eventTime": 1734425918, "webhookSchemaVersion": "1.0.0", "sourceType": "campaign", "campaignId": "cm4t1suns001uw6atri87v54s", "email": { "id": "cm4t1sseg004tje7982991nan", "emailMessageId": "cm4ittv1v001oow9hruou8na8", "subject": "Subject of the email" }, "contactIdentity": { "id": "cm4ittmhq0011ow9h6fb460yw", "email": "test@example.com", "userId": null } } ``` ### Testing #### testing.testEvent This is a test event that can be triggered at any time from the [Webhooks settings page](https://app.loops.so/settings?page=webhooks) in Loops. ```json theme={"dark"} { "eventName": "testing.testEvent", "eventTime": 1734425918, "message": "test", "webhookSchemaVersion": "1.0.0" } ``` # Workflows Source: https://loops.so/docs/workflows Workflows let you send emails based on something happening, like a contact property updating, a new contact being created or an external event happening in another platform. Our automation feature "Loops" has been renamed to "Workflows". ## Getting Started To start building your workflow, select a template or start from scratch. Workflow template selection Templates are added often and we're always open to taking suggestions! ## Building a workflow A workflow is an email sequence that can be triggered by different events and contain emails, delay timers and branches. You can add new nodes to your workflow by hovering over an arrow between nodes and clicking the `+` button. More complex workflows can be built by [adding branches](/docs/workflows/branching). Workflow builder canvas with nodes ## Zoom and canvas navigation You can zoom in and pan around the workflow canvas, which makes it easier to work with larger and multi-branch workflows. Zoom and panning uses typical trackpad and mouse gestures, so should be familiar. **On a trackpad** * Pinch to zoom in and out * Use two fingers to pan around the canvas **With a scroll-wheel mouse** * Use `⌘/Ctrl` and the scroll wheel to zoom * Use the scroll wheel to pan vertically * Use `Shift` and the scroll wheel to pan horizontally To reset your view back to 100%, click the "Zoom" button that appears at the bottom of the canvas. ## Mailing lists You can make a workflow send to a specific mailing list by using the dropdown in the top right of the workflow builder. By selecting lists from this dropdown you can make sure that only contacts from those lists are added to the workflow. Mailing list selector in workflow builder When using the **Contact added to list** trigger, this option is removed and you can instead select the list from the trigger node. ## Nodes There are six types of nodes available in workflows: * Trigger * Email * Timer * Audience filter * Branch ([read more](/docs/workflows/branching)) * Experiment ([read more](/docs/workflows/experiments)) ### Triggers Workflow trigger options The first node to add to a workflow is the trigger. This is what will send new people into the workflow. There are four trigger options: * **Contact added**: Triggered whenever a contact has been added to your audience. Only contacts who have been added via a [form](/docs/forms/simple-form), the [API](/docs/api-reference/intro) or an [integration](/docs/integrations) will be added based on this trigger. Contacts added individually to the audience table will not be included. Contacts added or updated via CSV will only trigger if you select the [Trigger workflows](/docs/add-users/csv-upload#trigger-workflows-via-csv) toggle during upload. * **Contact updated:** Triggered whenever a contact property changes from one value to another. Can also conditionally trigger only if the previous property matches the inputted value. * **Contact added to list:** Triggered whenever a contact is added to a certain [mailing list](/docs/contacts/mailing-lists). You can select the list from the trigger node. * **Event received**: Triggered when a certain [event](/docs/events) is sent to Loops by the API or an integration. With this trigger you can start a workflow based on interactions in your app. Common custom events are `signUp`, `canceled` and `activated`. You can read more about triggers [here](/docs/workflows/triggers). ### Emails You can add emails at any location inside your workflow, giving you flexibility to send mail immediately after the trigger or after a delay. You can also send to specific contacts by adding an audience filter, or by branching your workflows. Email node configuration in workflow builder You can add unlimited emails to each workflow. To edit an email, click on the email node, then **Edit email**. When a workflow is active, you cannot edit the email. Click **Pause** to pause the workflow and make changes to your email (and any other nodes). While paused, new contacts will be queued for up to 24 hours; they will enter your workflow as soon as you save your email by clicking **Resume**. You can copy emails from other workflows by clicking the **Copy existing email** button after creating a new email node. This will bring up a dropdown of emails from other workflows that you can insert into your workflow. Duplicating emails ### Timers Timer node in workflow builder A timer adds a fixed time period between two nodes in the workflow. For example, you could add a "3 day" timer after a "Contact added" trigger to send an email three days after a signup. You can select the “Immediately” option to bypass the timer or any increment of time to extend the duration of the workflow. You can add multiple timers to your workflows, to add delays in different parts of your workflow. ### Audience filters Audience filter node in workflow builder Audience filters let you create fine-tuned workflows to target specific contacts. For example, you can check in with contacts that have not signed up as a paying user 3 days after signing up by setting the Custom Event to `signup` the Timer duration to 3 days and the audience filter to `paid` equals `false`. There are two options for applying audience filters: * **All following nodes**: this will apply the filter when contacts reach every following node. If a contact no longer matches the filter's audience when they reach a following node, they will be removed from the workflow. * **Next node only**: this will apply your filter before contacts reach the next node. This is a one-time filter and any changes to the contact's value after the filter will not be taken into account. Learn how to branch workflows using audience filters. ## Metrics Click over to the **Metrics** tab to view simple metrics inline within the builder. Workflow metrics tab Click **View details** to view detailed metrics for your workflow. Workflow metrics details view ## Testing workflows You can test if a workflow works as expected by using email addresses with `@example.com` and `@test.com` domains (for example `test@example.com`). First, add these as contacts in your audience, then depending on how your workflows are set up, you can add and update properties, or send events to these email addresses to see how contacts move through your workflows. Emails will not be sent to `@example.com` or `@test.com` email addresses so this is a good method to test emailing contacts without affecting your sending domain’s reputation. You can also send test emails for each of the emails in a workflow. Click on an email node and then the **Send a preview** airplane icon above the preview that appears. # Branching workflows Source: https://loops.so/docs/workflows/branching Branching allows you to send different emails based on a contact's properties within a single workflow. Our automation feature "Loops" has been renamed to "Workflows". A common use case for crafting a user journey is to send different emails based on whether a contact has completed an action or not. For example, you may want to send a different email to free users versus paid users. You can do this by adding a **Branch** node to your workflow. Branches in a workflow ## Creating a branching workflow Adding a node to a branch To create a branching workflow, click the `+` icon where you want branching to be added and select the **Branch** node type. Two audience filter nodes are automatically created for you. Edit the audience filter settings for each to send different contacts down each branch, based on their properties. You can add as many branches as you like to your workflow. Add more branches by clicking the `+ Branch` option that appears on hover just below the Branch node. Each branch can contain emails, timers, audience filters, more branches, and experiments. Learn about the different node types. Add A/B testing to your workflows. ## Audience filters Every branch is defined by an audience filter, which is always the first node in a branch. Filters determine which contacts follow each branch. When a contact reaches a branch node, the audience filter nodes are evaluated left-to-right as defined in the workflow builder. The contact will proceed to the first matching filter and the remaining filters are skipped. If the contact matches none of the filters, they exit the workflow. You can create the equivalent of a "default path" by making the right-most filter have empty conditions so that it matches any contact. #### Upgrade notice Previously, a branch node would send contacts who matched multiple filters down multiple parallel branches. This behavior has been changed; now, in any new branches added to workflows, contacts will only follow the first branch they match. Branches with the old behavior are marked in your workflows. They will retain the old logic until you explicitly press the "Upgrade Branch Node" button shown in the Branch head node's options panel. For more information, see the [branch node upgrade documentation](/docs/workflows/branch-node-upgrade). The audience filter nodes created after a branching node can be toggled between two settings: * **All following nodes:** The audience filter will apply to *all nodes* downstream of the filter. If a contact stops matching the filter later down the branch, they will be removed from the workflow. * **Next node only:** The audience filter will only apply when contacts reach the next node in the branch. If a contact stops matching the filter later in the branch, they will remain in the workflow. Read more about audience filters. ## Converging branches A common use case is to have different branches converge back to the same path. For example, you may want to create branches with different filters that lead to different onboarding emails, but have all branches converge on a single timer that waits a week before sending the same follow-up email. Here's how you can set up converging paths in a workflow: Click **Reroute connection** located below a node. Click reroute button Now select the target node you want to reroute to. Since workflows do not allow circular paths, only valid target nodes will be visible, and any invalid target nodes will be dimmed. Click target node You'll end up with multiple nodes converging on the same target. Rerouting done ## Deleting branches To delete all branches, select the head Branch node and click the trash icon. This will remove all branches—including all nodes—following it. Delete branch node If you want to delete a single branch, you can do so by deleting each node in the branch you want removed. When the final node is deleted, the branch will be removed. Delete single branch # Experiments Source: https://loops.so/docs/workflows/experiments Learn how to use experiments to test different versions of your emails. Our automation feature "Loops" has been renamed to "Workflows". Experiments are a way to run split tests within workflows. This lets you test different versions of your emails, allowing you to see which version performs best. You can test things like subject lines, preheaders, and content, and specify the percentage of contacts who should be sent down each variant branch. ## Creating an experiment To add an experiment, add a new node by clicking the `+` button in the workflow builder and select **Experiment**. You can add an experiment to any workflow. Adding an experiment You can add multiple experiments to a workflow, even within other experiments if you want. Each experiment can contain any number of variants. Each variant can contain emails, timers and audience filters. Experiment with emails and filters ## Variants and controls Experiments are made up of variant and control branches. * **Variant** branches contain the email(s) you are testing, which will be sent to a specific percentage of contacts that you define. * The **Control** branch contains the email(s) that will be sent to all contacts not included in the experiment, and should be used as the baseline for the experiment. A control is optional; you can create an experiment without a control branch. An experiment node must be followed by at least one variant or control branch, otherwise you cannot start the workflow. Likewise, you must add at least one email within each control or variant branch before starting the workflow. ## Sample size You can specify the percentage of contacts who will be sent to control and variant branches by dragging the slider in Experiment nodes. Changing sample size Every variant you add will receive an equal percentage of contacts within the experiment sample. For example, if you set the sample size to 60% and add three variants and a control, 20% of contacts will be sent to each variant branch (60% / 3) and 40% of contacts will be sent to the control branch. You can change a variant to be the control by clicking on the variant node and toggling the **Use as control** option. Changing the control node If you do not add a control branch, only your selected sample size will be sent through the experiment node. For example, if you have three variants and no control and select 60% for your sample size, 40% of contacts entering the workflow will exit at the experiment node and not receive any emails. Experiment without a control ## Editing experiments Once you have started your workflow you can edit the number of contacts passing through each variant and control by first [pausing the workflow](/docs/workflows/pausing) from the top right of the workflow builder. Once you have paused the workflow, you can change the sample size, edit your variants and controls, and update any content. Click **Resume** to start the workflow again. When paused, email sending is stopped and contacts are queued behind their respective nodes as expected. All contacts that were scheduled to receive an email during the pause will receive it once you resume the workflow. ### Closing an experiment After some time testing, you may want to close experiment branches, to send all future contacts down your best-performing branch. To do this while retaining the metrics for all branches, first change your best-performing branch to a control branch (click its **Use as control** toggle; see image above). Then set the Experiment node sample size to 0%. This will change the Control size to 100%, sending all contacts down the new control branch. Close an experiment branch ## Results Once your experiment is running and contacts have flowed through the workflow, you can see the results by visiting the **Metrics** tab. Here you can compare your variants and view metrics for sends, opens and clicks. # Pausing workflows Source: https://loops.so/docs/workflows/pausing Learn how to pause and stop a workflow to control the sending of emails. Our automation feature "Loops" has been renamed to "Workflows". ## Pausing vs Stopping Both pausing and stopping a workflow will prevent any emails from sending until you resume the workflow, but there are a few key differences. We generally recommend stopping emails based on in-app actions completed (e.g., “user upgraded,” “ticket closed,” “trial converted”) rather than on email replies as a reply (such as an out of office email) doesn't necessarily mean the action you emailed about has been taken. ### Pausing a workflow When you pause a workflow, you can resume the workflow at any time. During the pause, all contacts that were scheduled to receive an email will receive it once you resume the workflow. However, this assumes that these contacts still meet the necessary criteria to receive the email (e.g., they are subscribed and match any audience filters or other conditions you've set). New contacts that match the Trigger conditions while the workflow is paused will enter the workflow but only within the first 24 hours. Contacts that qualify for the Trigger after the 24-hour mark will not enter the workflow. We will notify you via email once the 24-hour period has elapsed, reminding you that contacts will no longer queue to enter the workflow if you resume it. The 24-hour limitation prevents contacts from entering a workflow that has been paused for an extended period and receiving irrelevant emails. For instance, if your workflow sends a welcome email to new contacts, you wouldn't want a contact to join the workflow three months after signing up and receive a welcome email. ### Stopping a workflow Stopping a workflow will not queue any new contacts to enter the workflow. Any contacts that were queued to enter the workflow will not enter the workflow. That's the key difference between pausing and stopping a workflow. # Triggering workflows Source: https://loops.so/docs/workflows/triggers Learn how to trigger a workflow to start sending emails. Our automation feature "Loops" has been renamed to "Workflows". ## Workflow triggers A workflow trigger is an event, contact update or contact addition that starts a workflow. For example, if you create a workflow that sends a welcome email to new contacts, the trigger would be when a new contact is added to your audience. ## Different types of workflow triggers There are currently four types of triggers that you can use to start a workflow: **Contact added**, **Contact updated**, **Contact added to list** and **Event received**. ### Contact added The **Contact added** trigger will start a workflow when a contact is added to your audience. This trigger is useful for sending welcome emails to new contacts or sending a series of onboarding emails to new customers. This trigger works for contacts added via an integration, a form, or an API call. Contacts uploaded via CSV or added individually to the audience table will not be included. As long as the contact is added to your audience with an automatic method the workflow will start. This trigger requires no additional setup. Once you create a workflow with this trigger, you can start adding contacts to your audience and contacts will enter the workflow. ### Contact updated The **Contact updated** trigger will start a workflow when a contact is updated in your audience. This trigger is useful for sending emails to contacts based on their actions or behavior. For example, you can send a series of emails to contacts who change their subscription plan from free to paid or from paid to canceled. You can also set the trigger to only start the workflow when a specific field is updated from a specific value to another specific value. For example, you can send a series of emails to contacts who change their subscription plan from free to paid but exclude contacts who have updated their subscription plan from paid to canceled. ### Contact added to list The **Contact added to list** trigger will start a workflow when a contact is added to a [mailing list](/docs/contacts/mailing-lists). It triggers every time a contact is added to a list (so if the contact is removed from a list and then re-added, it will trigger again). This trigger is tied directly to a contact's subscription to the selected mailing list. If the contact is removed from the mailing list, they will be removed from any connected workflows when they reach the next node. ### Event received The **Event received** trigger will start a workflow when a contact receives a specific matching event sent via API, Integrations or a form. This trigger is great for events like payment received, order placed, or a new message received. You can fire events with the [Events API](/docs/api-reference/send-event) or an integration. ## Trigger frequency You can choose to trigger a workflow just the first time a contact matches the trigger settings or every time the contact matches. Trigger frequency For example, if you want to send to a contact every time they update their subscription plan, you can choose to trigger the workflow every time the contact is updated (select "Every time"). However, if you want to send a welcome email to a contact just the first time they are added to your audience, you can choose to trigger the workflow once when the contact is added (select "One time"). ## Changing the trigger type You can change the trigger type at any time. For example, if you create a workflow with the Contact Added trigger, you can change it to the Contact Updated trigger at any time. ## Re-triggering workflows Sometimes you may want to re-trigger a workflow for certain contacts, for example if you want to add an older contact to a new email sequence. The easiest way to do this is to [download contacts in a CSV](/docs/contacts/export-contacts) and re-upload them with the [CSV uploader](/docs/add-users/csv-upload#trigger-workflows-via-csv). Make sure to toggle **Trigger workflows** in the last upload step.