# Brevo

Source: https://developers.swell.is/app-guides/brevo

The Brevo app keeps your store's customers in sync with Brevo contacts, and sends cart and order events you can use to trigger Brevo automations. Customers who accept email marketing are added to your email list, and customers who accept SMS marketing can have their phone number saved for SMS.

With order sync on, orders also go to Brevo eCommerce for revenue reporting and segmentation. An action in the dashboard imports the customers you already have.

## How it works

| When | What the app does |
| --- | --- |
| A customer account is created, or its email, name, phone, addresses or opt-ins change | Creates or updates the Brevo contact, and adds it to your email list if the customer accepts email marketing. |
| A customer turns off email marketing | Removes the contact from your email list, if Remove from list on opt-out is on. |
| A cart is created | Sends checkout_started, if the cart already has a customer. |
| Swell marks a cart abandoned | Sends cart_abandoned. |
| An order is placed | Sends order_submitted, and the order to Brevo eCommerce if order sync is on. |
| An order is fully fulfilled | Sends order_fulfilled, and updates the order in Brevo eCommerce if order sync is on. |
| An order is canceled | Sends order_canceled, and updates the order in Brevo eCommerce if order sync is on. |
| You run Import existing customers | Creates or updates a contact for every customer account, in the background. |

Events and orders are sent only for carts and orders linked to a customer account with an email. Each event can be turned off in the app settings, and nothing is sent while **Enabled** is off.

## Before you begin

- A Brevo account and a Brevo API key. You create the key in step 1.
- Blocking of unknown IP addresses turned off in Brevo, under **Security > Authorized IPs**. Swell has no fixed IP addresses to add, and while blocking is on, Brevo refuses every call from the app. Brevo turns it on by default for accounts created since May 2024.
- A Brevo list for customers who accept email marketing. Without a list, customers are synced as contacts but aren't added to one.
- The names of the first and last name contact attributes in your Brevo account, if they aren't `FIRSTNAME` and `LASTNAME`.
- For SMS marketing, Brevo's `SMS` contact attribute, where the app saves the customer's phone number.
- For order sync, eCommerce activated in your Brevo account.
- Opt-ins at checkout. Customers accept marketing with **Email opt-in** and **SMS opt-in** in **Settings > Checkout** in Swell. **SMS opt-in** is set to **Disable and hide** by default, so change it to collect SMS opt-ins.

## Set up

Install Brevo from the Swell App Store, and set it up in your test environment before the live one.

**1. Create an API key**

In Brevo, go to **Settings > SMTP & API > API Keys**, create a key, and copy it. The key starts with `xkeysib-`.

Then go to **Security > Authorized IPs** and turn off blocking of unknown IP addresses. Swell has no fixed IP addresses to add to the list.

**2. Find your list ID**

In Brevo, go to **Contacts > Lists** and find the list for customers who accept email marketing. Its ID is the number after `#`, such as `7`.

**3. Add them to the app**

In Swell, go to **Apps > Brevo > Settings** and paste the key in **API key**. **Enabled** is on by default. On the **Contacts** tab, enter the **Email list ID**, turn on **Sync phone for SMS opt-ins** if you send SMS, and change **First name attribute** and **Last name attribute** if your Brevo account uses other names. On the **Events** tab, turn off the events you don't use. To send orders, turn on **Sync orders to Brevo eCommerce** on the **eCommerce** tab. Click **Save settings**.

Then choose **Actions > Test connection**. The message names your Brevo account and your list. From then on, new and changed customers and their events are sent as they happen.

**4. Import your existing customers**

Choose **Actions > Import existing customers**, then **Start import**. The import runs in the background and creates or updates a contact for every customer account, with the settings you saved.

**5. Create a test customer**

Go to **Customers** and create a customer with an email you can check, with **Customer accepts email marketing** turned on. In Brevo, the contact appears under **Contacts** with the customer's name, in your list. The logs in Swell show `Brevo: synced contact` with the email and the list ID. Then place a test order as that customer, and the logs show `Brevo: tracked order_submitted` with the email.

**6. Go live**

Settings are saved separately for each environment. In the live environment, repeat steps 3 and 4 with the API key and list your store uses. Test customers and orders go to the Brevo account whose key you enter, so use a separate Brevo account or list for the test environment.

## Using Brevo

### Automations from events

Each event reaches Brevo with its name below and the customer's email, so you can use it to trigger automations. For example, trigger cart recovery emails on `cart_abandoned`, order follow-ups on `order_submitted` and review requests on `order_fulfilled`.

| Brevo event | Sent when | Setting |
| --- | --- | --- |
| checkout_started | A cart is created, if it already has a customer. | Checkout started |
| cart_abandoned | Swell marks a cart abandoned. | Cart abandoned |
| order_submitted | An order is placed. | Order placed |
| order_fulfilled | An order is fully fulfilled. | Order delivered |
| order_canceled | An order is canceled. | Order canceled |

Each event's properties are the cart's or order's `id`, `number`, `status`, `currency`, `grand_total`, `sub_total`, `discount_total`, `tax_total`, `shipment_total`, `coupon_code` and `items`, and for carts `checkout_url`, which brings the shopper back to their cart. Each item has `id`, `product_id`, `variant_id`, `name`, `slug`, `sku`, `image_url`, `quantity`, `price`, `price_total` and `discount_total`. Amounts are in the record's currency, not in cents. Properties without a value in Swell are left out.

Each event also sends the customer's first and last name as contact properties, under the attribute names in the settings.

### Cart events

A cart is usually created when the shopper adds the first item. At that point most carts don't have a customer yet, so `checkout_started` is sent only for carts that do, such as a cart started by a customer who's logged in.

Swell marks a cart abandoned 3 hours after its items, shipping or billing details last changed, or after the delay set for the abandoned cart email in **Settings > Notifications**. Swell marks carts abandoned whether that email is on or off. A cart that's abandoned again after the shopper comes back sends `cart_abandoned` again. If Brevo sends your cart recovery emails, turn off **Abandoned cart** in **Settings > Notifications**, so customers don't get both.

### Contacts and your email list

Each customer account with an email becomes a Brevo contact, identified by email, with the first and last name. Account changes that don't affect the contact, such as a new order count, aren't sent.

- A customer who accepts email marketing is added to the **Email list ID** list.
- A customer who turns off email marketing in Swell is removed from the list, if **Remove from list on opt-out** is on. The contact stays in Brevo.
- Customers who didn't opt in are synced as contacts but not added to the list.
- The app never blocklists or unsubscribes contacts in Brevo, and unsubscribes in Brevo don't reach Swell.

### SMS phone numbers

With **Sync phone for SMS opt-ins** on, a customer who accepts SMS marketing gets their phone number in the contact's `SMS` attribute. The app uses the account's phone number, or the phone number of its shipping or billing address. Numbers without a country code use the shipping country, or the billing country, and are sent in international format, such as `+15035550142`. Numbers that aren't valid are left out.

If Brevo refuses the number, for example because another contact already has it, the contact is saved without it. A customer who turns off SMS marketing in Swell keeps their number in Brevo.

### Importing existing customers

To create contacts for the customers you already have, go to **Apps > Brevo**, choose **Actions > Import existing customers**, and click **Start import**. To import only customers who accept email marketing, check **Only customers who opted in to email** first.

- The import runs in the background, and a message tells you when it has finished or failed. Runs are listed on the app's **Workflows** tab as Import customers, with their status. Click a run to see its steps or cancel it.
- Customers are imported 100 at a time, oldest first. The settings are read again before each 100, so changes you save during an import apply to the next 100. Customers who accept email marketing are added to your list, and the others are created or updated as contacts without it.
- Customers without an email are skipped. Contacts that Brevo refuses when they're created or updated without the list, such as ones with an invalid email, are logged and counted as failed, and the import continues. If Brevo refuses a group's import to your list, the error is logged and those customers are created or updated one by one instead, with the list, and counted as synced or failed.
- Running the import again updates contacts without duplicating them, and doesn't clear values already in Brevo. It doesn't remove anyone from your list.
- The import sends customers only. Past carts and orders aren't sent as events or eCommerce orders.
- If **Enabled** is off or the API key is empty, the import doesn't start and fails with the reason. If you turn off **Enabled** or remove the API key during an import, it finishes the 100 customers it's sending, stops before the next 100, and fails with the totals sent until then.
- When it finishes, the logs show the totals: added to list, synced, skipped and failed.

**Import existing customers** is in the Actions menu only while **Sync customer accounts** is on.

### Order sync

With **Sync orders to Brevo eCommerce** on, each order is sent to Brevo eCommerce when it's placed, and sent again when it's fully fulfilled or canceled. Each order has its order number, total, creation and update dates, the customer's email, the coupon, and each item's product ID, variant ID, price and quantity.

The order's status is its status in Swell, such as `payment_pending`, `delivery_pending`, `complete` or `canceled`. Brevo's copy is updated only at those three moments, so payments, refunds and edits in between aren't sent. Orders are sent whether or not their events are turned on.

### In the dashboard

| Where | What it's for |
| --- | --- |
| Apps > Brevo > Settings | The API key, Enabled, and the Contacts, Events and eCommerce tabs. |
| Apps > Brevo > Actions | Test connection, and Import existing customers. Save the settings first: the actions are unavailable while there are unsaved changes. |
| Apps > Brevo > Workflows | The runs of Import existing customers, with their status, who started them, when, and how long they took. |
| Customers | Accepts email marketing and Accepts SMS marketing on each customer. |
| Settings > Checkout | The email and SMS opt-ins customers accept at checkout. |
| Settings > Notifications | The abandoned cart delay, and Swell's own abandoned cart email. |
| Developer > Console > Logs | Each contact, event and order the app sends, and each error. |

## Settings

Settings are in **Apps > Brevo > Settings** and are saved separately for each environment.

**API key** and **Enabled** are at the top. The other settings are on the **Contacts**, **Events** and **eCommerce** tabs.

| Setting | Description | Default |
| --- | --- | --- |
| API key | Your Brevo API key, from Brevo > Settings > SMTP & API > API Keys. Starts with xkeysib-. Required: without it, the app sends nothing. | None |
| Enabled | Turns all syncing on or off without uninstalling the app. While it's off, the import doesn't run. | On |
| Contacts > Sync customer accounts | Creates and updates a Brevo contact for each customer account. Import existing customers is available only while it's on. | On |
| Contacts > Email list ID | The ID of the Brevo list for customers who accept email marketing, such as 7 or #7. When it's empty or not a number, contacts aren't added to a list. | None |
| Contacts > Remove from list on opt-out | Removes the contact from the list when the customer turns off email marketing. | On |
| Contacts > Sync phone for SMS opt-ins | Saves the phone number in the contact's SMS attribute for customers who accept SMS marketing and have a valid number. | Off |
| Contacts > First name attribute | The Brevo contact attribute for the first name. Saved in uppercase. | FIRSTNAME |
| Contacts > Last name attribute | The Brevo contact attribute for the last name. Saved in uppercase. | LASTNAME |
| Events > Checkout started | Sends checkout_started when a cart is created. | On |
| Events > Cart abandoned | Sends cart_abandoned when a cart is abandoned. | On |
| Events > Order placed | Sends order_submitted when an order is placed. | On |
| Events > Order delivered | Sends order_fulfilled when an order is fully fulfilled. | On |
| Events > Order canceled | Sends order_canceled when an order is canceled. | On |
| eCommerce > Sync orders to Brevo eCommerce | Sends orders to Brevo eCommerce. Activate eCommerce in Brevo first. | Off |

## Limitations

- Data goes one way, from Swell to Brevo. Unsubscribes and edits made in Brevo don't reach Swell.
- Contacts are identified by email. When a customer changes their email, a new contact is created for the new address, and the old one stays in Brevo.
- Turning off email marketing removes the customer from your list only. Turning off SMS marketing doesn't remove the number from Brevo.
- One list for email opt-ins. SMS opt-ins aren't added to a list.
- Carts and orders without a customer email send nothing. Most new carts don't have one yet, so `checkout_started` is sent mainly for customers who are logged in.
- The import sends customer accounts only. Past carts and orders aren't sent, and changes made while the app was off aren't sent later.
- The app doesn't send your product catalog to Brevo. eCommerce orders refer to items by their Swell product and variant IDs.
- Brevo accepts up to 50 KB of event properties, so an event for a cart or order with a very long item list can be refused.
- Events have no ID, so the app sends each event at most once. It sends an event again only when Brevo rate-limits the app, because Brevo records nothing then. After a network error, timeout or server error, Brevo may already have recorded the event, so it isn't sent again and can be lost. If Swell runs the function again for another reason, for example after it times out, Brevo records the event again.
- If Brevo can't be reached, rate-limits the app or has a server error while a contact or order is sent, Swell runs the function again, first after a minute and then at growing intervals. If it keeps failing for 4 days, Swell turns off the function and emails the store's admins. To turn it back on, update or reinstall the app. Other errors are logged and the change isn't sent again.
- Event names and properties are fixed.

## Troubleshooting

**Test connection** and **Import existing customers** show their messages in the dashboard. The app also logs each contact, event and order it sends, and each error. In the Swell dashboard, the logs are under **Developer > Console > Logs**.

### Nothing arrives in Brevo

Check that **Enabled** is on and the **API key** is filled in, and click **Save settings**. The logs show "Brevo: missing API key in app settings" when the key is empty. Then choose **Actions > Test connection**. Changes made while the app was off aren't sent later. To bring contacts up to date, run **Import existing customers**.

### "Add your Brevo API key and save the settings first."

**Test connection** checks the saved settings. Enter the key and click **Save settings** before you test.

### "Brevo rejected the API key. Check it in Brevo → Settings → SMTP & API → API Keys."

Copy the key again from **Settings > SMTP & API > API Keys** in Brevo, and check that it starts with `xkeysib-` and hasn't been deleted. Paste it, click **Save settings**, and test again.

### "Brevo blocked the request because it came from an IP address that is not authorized. …"

Brevo's **Authorized IPs** check is on, so Brevo refuses calls from the app even with a valid key. Swell has no fixed IP addresses to add. In Brevo, go to **Security > Authorized IPs**, turn off blocking of unknown IP addresses, and run **Test connection** again. While it's on, every sync and event fails with a 401 in the logs, and the import counts every customer as failed.

### "Email list #… was not found in Brevo."

Check the **Email list ID** against **Contacts > Lists** in Brevo. The message shows only when Brevo answers that the list doesn't exist.

### "Email list #… could not be checked: …"

Brevo answered the list check with an error, and its reason follows the message. When the message says `try again`, Brevo couldn't be reached, rate-limited the app or had a server error: test again in a minute.

### "Brevo request failed: …"

Brevo answered with another error or couldn't be reached. The reason follows the message. Try again in a minute.

### Customers aren't added to the list

Check that the customer accepted email marketing: the Swell customer page shows **Accepts email marketing**. Then check that **Sync customer accounts** is on and the **Email list ID** is right. Customers who existed before you set up the app are added when you run **Import existing customers**.

### Contacts don't have an SMS number

Check that **Sync phone for SMS opt-ins** is on, that the customer page shows **Accepts SMS marketing**, and that the customer has a valid phone number with a country code, or a shipping or billing country. The logs show "Brevo: SMS rejected for …, retrying without it" when Brevo refuses the number, for example because another contact has it.

### Names are missing on contacts

Check that **First name attribute** and **Last name attribute** match the names of the contact attributes in your Brevo account.

### Some cart or order events don't arrive

The logs show "Brevo: checkout_started skipped", or the name of another event, followed by "has no customer email" when the cart or order isn't linked to a customer with an email. Also check that the event is turned on in the **Events** tab. `cart_abandoned` arrives only after Swell marks the cart abandoned.

An event that failed with a network error, timeout or Brevo server error is logged with "(not retried, Brevo may have recorded it)" and isn't sent again.

### Orders don't appear in Brevo eCommerce

Check that **Sync orders to Brevo eCommerce** is on and that eCommerce is activated in Brevo. Orders are sent when they're placed, fully fulfilled or canceled after you turn it on. The logs show the reason when Brevo refuses an order, after "order sync for order" and the order ID.

### An import finished, but no contacts were created

When Brevo refuses the import to your list for a group of customers, the logs show "Brevo: import job for … contacts rejected, syncing them one by one:" with Brevo's reason, and those customers are created or updated one by one instead. If that fails too, for example because the list was deleted, they're counted as failed. Check the **Email list ID** and the Authorized IPs setting with **Actions > Test connection**, then run the import again.

Brevo adds the customers who accept email marketing to your list in a background job of its own, so the logs don't show contacts that Brevo refuses there, and the totals count them as added to list.

### "Import not started: …"

The import fails without sending anyone, with "Import not started: the Brevo app is disabled in its settings." when **Enabled** is off, or "Import not started: add your Brevo API key in the app settings." when the API key is empty. Fix the setting, click **Save settings**, and run the import again.

### "Import stopped: …"

**Enabled** was turned off or the API key was removed while the import was running. The import finished the 100 customers it was sending and stopped before the next 100. The message gives the reason and the totals sent until then, such as "Import stopped: the app was disabled in settings. Before stopping: added to list 100, synced 0, skipped 0, failed 0." Fix the setting, click **Save settings**, and run the import again. Customers already sent are updated, not duplicated.

### "Import existing customers" failed

Open the run on the **Workflows** tab to see its error and the step that failed. On temporary errors, such as when Brevo can't be reached, rate-limits the app or has a server error, each group of 100 customers is tried up to 5 more times before the import fails. Running the import again is safe.

### Import existing customers isn't in the Actions menu

Turn on **Sync customer accounts** and click **Save settings**.

## Uninstalling

Uninstalling the app stops syncing contacts and sending events and orders to Brevo, so automations triggered by the app's events don't start for new activity. It removes the app's functions and settings, including the API key, and the **Test connection** and **Import existing customers** actions. Uninstalling applies to the environment you are in, so remove the app from the test and live environments separately.

Contacts, list members, events and orders already in Brevo stay there. The app isn't notified when it's uninstalled, so if you no longer need the API key, delete it in Brevo under **Settings > SMTP & API > API Keys**. To pause syncing without uninstalling, turn off **Enabled**.

## For developers

| Function | Runs on | What it does |
| --- | --- | --- |
| contact-sync | account.created and account.updated | Reads the account and creates or updates the contact. Skips updates that change none of email, first_name, last_name, phone, email_optin, sms_optin, shipping and billing. Unlinks the list when email_optin changed to false. |
| cart-events | cart.created and cart.abandoned | Reads the cart with its account and items, and sends checkout_started or cart_abandoned. |
| order-events | order.submitted, order.delivered and order.canceled | Reads the order with its account and items, sends the order to Brevo eCommerce if order sync is on, then sends order_submitted, order_fulfilled or order_canceled. |
| test-connection | The Test connection action | Reads the Brevo account and the email list. |
| import-customers | The Import existing customers action | A workflow. Reads accounts 100 at a time in id order, one step per page, each step retried up to 5 times with exponential backoff from 30 seconds. |

The app adds no fields to Swell records. It reads its settings from `brevo` in the app settings, applies the defaults to values not saved yet, upper-cases the attribute names, and accepts the list ID as `7` or `#7`. Every function except `test-connection` stops when `enabled` is `false` or `api_key` is empty.

All calls go to `https://api.brevo.com/v3` with the `api-key` header:

- `POST /contacts` with `updateEnabled: true`, the name attributes, `SMS` when allowed, and `listIds` for an account with `email_optin: true` when a list is set. On a 400 response with `SMS` in the body, it's sent once more without `SMS`.
- `PUT /contacts/{email}?identifierType=email_id` with `unlinkListIds` on opt-out. A 400 or 404 response is logged as nothing to remove.
- `POST /events` for cart and order events.
- `POST /orders/status` for eCommerce orders.
- `POST /contacts/import` for the import's opted-in accounts when a list is set, one job per page, with `updateExistingContacts: true`, `emptyContactsAttributes: false` and `disableNotification: true`. The app logs the job's `processId` and doesn't wait for its result. A 4xx response other than 429 is logged, the page's opted-in accounts count as `failed`, and the import continues. Other accounts are sent with `POST /contacts`, 5 at a time.
- `GET /account` and `GET /contacts/lists/{id}` for Test connection.

An event payload looks like this. No `event_date` is sent, and `contact_properties` is left out when the account has no name:

```json
{
  "event_name": "order_submitted",
  "identifiers": { "email_id": "maya.chen@example.com" },
  "contact_properties": { "FIRSTNAME": "Maya", "LASTNAME": "Chen" },
  "event_properties": {
    "id": "6acc9e5f3b1d2a0012f4c771", "number": "1042", "status": "delivery_pending", "currency": "USD",
    "grand_total": 128, "sub_total": 118, "discount_total": 0, "tax_total": 10, "shipment_total": 0,
    "items": [{
      "id": "6acc9e5f3b1d2a0012f4c772", "product_id": "66e1a7c09b3f2a0012c5d8e5",
      "name": "Oak Desk Lamp", "slug": "oak-desk-lamp", "sku": "LAMP-OAK",
      "image_url": "https://cdn.schema.io/alder/lamp.jpg",
      "quantity": 1, "price": 118, "price_total": 118, "discount_total": 0
    }]
  }
}
```

The eCommerce order has `id` (the order number, or the order ID without one), `status` (the Swell order status, not mapped), `amount` (`grand_total`), `createdAt` and `updatedAt` (from `date_created` and `date_updated`, in `YYYY-MM-DDTHH:mm:ssZ`), `identifiers.email_id`, `products` with `productId`, `variantId`, `price` and `quantity`, and `coupons` with the coupon code.

SMS numbers are parsed with libphonenumber-js's `parsePhoneNumberWithError`, using the account's `phone`, then `shipping.phone`, then `billing.phone`, with `shipping.country` or `billing.country` as the default country. Numbers that fail to parse or `isValid()` are dropped, and the rest are sent as E.164.

For contacts and eCommerce orders, network errors, 429 and 5xx responses are rethrown, so Swell retries the function. Events are rethrown only on a 429 (`handleEventError`): after a network error or 5xx Brevo may have recorded the event, so it's logged with `(not retried, Brevo may have recorded it)`. Other errors are logged with `console.error` and the function succeeds. In `order-events`, the order sync runs first, then the event. A retryable order sync failure is rethrown before the event is sent, and a retry repeats the order sync, which updates the same order. A 401 whose message mentions an IP address or `authorised_ips` is Brevo's Authorized IPs check (`BrevoError.ipBlocked`), which Test connection reports apart from a rejected key.

The step `load settings` returns only `enabled` and `has_api_key`, so the API key isn't stored in the run's step output. Each page step reads the settings again and uses them for that page. The workflow returns `imported`, `upserted`, `skipped` and `failed`. When `enabled` is `false` or `api_key` is empty, it throws instead, so the run ends as failed: with `Import not started: …` before the first page, or with `Import stopped: …` and the totals so far when a page step finds the app disabled or the key removed. The modal's `only_optin` value arrives in `req.data`.

## Support

Swell builds and maintains the Brevo app. For help with the app, email support@swell.is. For questions about your Brevo account, automations or lists, contact Brevo.
