---
url: /integrations/brevo.md
---
# Brevo

Brevo (formerly Sendinblue) appears in three places: as the push target of the [email campaigns](/modules/email-campaigns) module, as the contact directory that form leads are synced to (since 2.17), and possibly as the SMTP relay for the site's transactional mail. Campaign push and contact sync share the API key and `EMAIL_DRIVER=brevo`; the SMTP relay is configured separately. Both need the email module, which is off by default since 2.22 (`EVOLVE_MODULE_EMAIL=true`).

## Campaign push

What you need from the Brevo account:

1. An **API key** (v3) from SMTP & API > API keys. Keys are shown once.
2. A **verified sender** (Senders & IP). The address in `EMAIL_SENDER_EMAIL` or on the campaign must be one of them, otherwise Brevo rejects the campaign.
3. The **contact lists** the campaign will go to. The push does not touch lists; recipients are picked in Brevo after the push.

| Variable | Notes |
|---|---|
| `EMAIL_DRIVER=brevo` | turns the push on |
| `EMAIL_BREVO_API_KEY` | or `BREVO_API_KEY`, which some projects already have for SMTP |
| `EMAIL_SENDER_NAME`, `EMAIL_SENDER_EMAIL` | default sender |
| `EMAIL_MERGE_DIALECT=brevo` | the default; keeps `{{ unsubscribe }}`, `{{ mirror }}` and `{{ update_profile }}` as Brevo expects |

The push creates or updates a campaign draft in Brevo. Scheduling and sending stay in Brevo.

## Contact sync&#x20;

With `EMAIL_DRIVER=brevo`, every form whose **Marketing** card has the sync switched on turns its leads into Brevo contacts: on the chosen lists, with the mapped attributes, and with marketing consent from a checkbox or a double opt in. Unsubscribes, bounces and spam complaints come back through a webhook. How editors set up a form is described in [Forms and leads](/panel/forms-and-leads#marketing-contact-sync), what happens to contacts afterwards in [Email campaigns](/modules/email-campaigns#contacts).

Setting up an account takes these steps, in this order.

### 1. API key

The same v3 key as for the campaign push (`EMAIL_BREVO_API_KEY`, falling back to `BREVO_API_KEY`). If the account limits API access to authorised IP addresses (Security > Authorised IPs), add the outgoing IP of the web server. Otherwise Brevo answers 401 and the health check reports the key as rejected, although the key itself is fine.

### 2. Phone verification

New Brevo accounts must verify a phone number before Brevo sends any transactional mail, and the double opt in confirmation is one. Until then the API answers `403 SMTP account not activated`. Evolve records each refused confirmation as a sync failure with that message, and the health check reports that Brevo has not activated the transactional mail relay (only while a form uses double opt in). Verify the number in the Brevo account, then run `php artisan email:retry-sync` to send the confirmations that were held back.

### 3. Sender domain

Authenticate the sender domain in Brevo (Senders, domains: SPF, DKIM, DMARC). The health check looks at the domain of `MAIL_FROM_ADDRESS` and reports it when Brevo does not know the domain or has not authenticated it. Use an address on an authenticated domain as the sender of the double opt in template as well.

### 4. Double opt in template

Only needed when a form uses double opt in. In Brevo, create a template of type **Double opt-in**. Its button or link must point at `{{ doubleoptin }}`, Brevo replaces it with the confirmation link. Put the template's ID into `EMAIL_BREVO_DOI_TEMPLATE_ID`. The target after the click is set by Evolve with every request (a signed link back to the site), the template needs no redirect of its own.

Without the ID, double opt in leads are stored but fail to sync ("No double opt in template configured"), and the health check names the missing variable.

### 5. Attributes and lists

```bash
php artisan email:setup --dry-run   # show what would be created
php artisan email:setup
```

`email:setup` creates the contact attributes Evolve sends, if the account lacks them, and can be run again at any time:

| Attribute | Type | Content |
|---|---|---|
| `SOURCE_SITE` | text | `EMAIL_CONTACTS_SITE`, else the slug of `APP_NAME`. Tells the contacts of several sites in one account apart. |
| `SOURCE_FORM` | text | name of the form the lead came from |
| `SOURCE_DATE` | date | day of the sync |
| `LANG` | text | language the form was submitted in |
| `CONSENT_MARKETING` | boolean | `true` with consent, `false` after an unsubscribe |
| `CONSENT_DATE` | date | day the consent was given or confirmed |
| `CONSENT_TEXT_VERSION` | text | hash of the consent checkbox text the visitor saw |

Attributes registered by modules (for example the computed attributes of a [form type](/custom-modules/extending-evolve#form-types)) are created too. Given and family name are Brevo's own attributes and are not created: they go to `FIRSTNAME` and `LASTNAME`, or to the names in `EMAIL_CONTACTS_FIRSTNAME_ATTRIBUTE` and `EMAIL_CONTACTS_LASTNAME_ATTRIBUTE` for accounts that use others (such as `VORNAME` and `NACHNAME`). Brevo silently drops attributes it does not know, so check these names once per account.

Lists are picked per form from the lists the account already has. To have `email:setup` create lists, name them in `contacts.lists` in `config/email.php` (publish tag `email.config`); they are created in the folder "Evolve", or in the first existing folder.

At the end the command prints the manual steps with the webhook URL of this install.

### 6. Webhook

Generate a random secret (`openssl rand -hex 32`) and set it as `EMAIL_BREVO_WEBHOOK_SECRET`. Then add a **marketing** webhook in Brevo:

* URL: `https://<domain>/api/email/webhook?token=<secret>`
* Events: unsubscribed, hard bounce, soft bounce, spam, contact deleted

Brevo does not sign its webhook calls, so the secret in the URL is the protection. Without a secret the endpoint answers 503, with a wrong token 403. To accept calls only from Brevo's servers in addition, put the IP ranges Brevo publishes for its webhooks into `EMAIL_BREVO_WEBHOOK_ALLOWED_IPS`, comma separated in CIDR notation; calls from other addresses get 403. Behind a proxy or load balancer this only works when Laravel sees the real client IP (trusted proxies).

Without the webhook, unsubscribes and bounces in Brevo never reach the site, and its contacts keep showing those addresses as active. What each event does is listed in [Email campaigns](/modules/email-campaigns#webhook-events).

### Variables

| Variable | Default | Effect |
|---|---|---|
| `EMAIL_DRIVER` | unset | `brevo` turns on campaign push and contact sync. `log` writes contact operations to the log instead, for local work. Unset: no contact sync, no Marketing card settings. |
| `EMAIL_BREVO_API_KEY` | falls back to `BREVO_API_KEY` | API key (v3). |
| `EMAIL_BREVO_BASE_URL` | `https://api.brevo.com/v3` | API base. |
| `EMAIL_BREVO_WEBHOOK_SECRET` | unset | Token in the webhook URL. Unset, the webhook answers 503. |
| `EMAIL_BREVO_WEBHOOK_ALLOWED_IPS` | empty | Comma separated CIDR ranges the webhook accepts. Empty accepts any address with a valid token. |
| `EMAIL_BREVO_DOI_TEMPLATE_ID` | `0` (none) | ID of the double opt in template. |
| `EMAIL_CONTACTS_SITE` | slug of `APP_NAME` | Value of `SOURCE_SITE`. |
| `EMAIL_CONTACTS_MAIN_LIST` | unset | Brevo list ID whose newest contacts the [email overview](/modules/email-campaigns#overview-and-contacts-in-the-panel) shows. Unset: the newest contacts of the account.  |
| `EMAIL_CONTACTS_FIRSTNAME_ATTRIBUTE`, `EMAIL_CONTACTS_LASTNAME_ATTRIBUTE` | `FIRSTNAME`, `LASTNAME` | Brevo attributes for given and family name. |
| `EMAIL_ALERT_ADDRESS` | falls back to `HEALTH_WEBMASTER_ADDRESS` | Receives sync alerts and spam complaints. Sent through the site's own mailer, never through Brevo. |

In `config/email.php` only: `contacts.lists` (lists `email:setup` creates, default none), `contacts.soft-bounce-threshold` (5), `contacts.alert-after-attempts` (10), `contacts.request-log-retention-days` (30, `0` turns the request log off), `brevo.requests-per-second` (10) and `brevo.retry-backoff-ms` (`[1000, 2000]`, the short retry for 429, 5xx and network errors before a request counts as failed).

### Health check

The **Email directory** check runs hourly and shows in Settings > Health. Without a driver it reports `off`. Otherwise it is green with "Contact directory reachable and set up", or a warning that names every problem it found:

| Message | Cause |
|---|---|
| No Brevo API key configured | `EMAIL_BREVO_API_KEY` and `BREVO_API_KEY` are empty. |
| Brevo rejects the API key | 401 or 403 from Brevo: wrong key, or an IP restriction (step 1). |
| Brevo is not reachable | Network or server error, with Brevo's message. |
| A form uses double opt in, but `EMAIL_BREVO_DOI_TEMPLATE_ID` is not set | Step 4. |
| A form uses double opt in, but Brevo has not activated the transactional mail relay | Step 2, the phone verification. |
| No webhook secret | Step 6. |
| The sender domain is not set up in Brevo / is not authenticated | Step 3, for the domain of `MAIL_FROM_ADDRESS`. |
| The account lacks attributes | Run `php artisan email:setup` (step 5). |
| Contact sync failures are open | Leads that did not reach Brevo, see [retries](/modules/email-campaigns#failures-and-retries). |

The messages follow the panel language.

### Staging

Brevo sends the double opt in mail itself, so the [mail redirect](/getting-started/mail) of staging does not apply to it, and every synced lead becomes a real contact in the account. Point staging at a separate Brevo test account, or leave `EMAIL_DRIVER` unset or on `log` there. The alert mails go through the site's mailer and are redirected as usual.

### Verifying

1. `php artisan email:setup --dry-run` lists the attributes, then the health check is green.
2. Switch on the sync for a form, pick a list, keep consent on Checkbox. Submit it with the consent box ticked. In Brevo the contact is on the list with `SOURCE_SITE`, `SOURCE_FORM`, `LANG` and the `CONSENT_*` attributes. The lead drawer shows "Synchronisiert" with the time.
3. Switch to double opt in and submit with a new address. The confirmation mail arrives from Brevo, the lead shows "Bestätigung ausstehend". After the click the lead shows "Marketing erlaubt" with the consent time and the contact is on the list.
4. Unsubscribe the test contact through a campaign mail or delete it in Brevo. With the webhook set up, the contact's status in the `email_contacts` table changes to `unsubscribed` (a deleted contact is removed there). To test the endpoint without Brevo, simulate the call:

   ```bash
   curl -s -X POST "https://<domain>/api/email/webhook?token=<secret>" \
     -H 'Content-Type: application/json' \
     -d '{"event":"unsubscribed","email":"you+test@example.com"}'
   ```

   The answer is `{"status":"ok"}`.

## Transactional mail

A site may also send its own mail (form notifications, event confirmations, health check mails) through Brevo's SMTP relay. That is ordinary Laravel mail configuration, `MAIL_MAILER=smtp` with Brevo's host and the SMTP key from the same account, see [Mail](/getting-started/mail). Neither the campaign push nor the contact sync depends on it.

## Verifying the campaign push

After a push, the campaign appears in Brevo under Campaigns > Email as a draft, with the preview text and sender filled in. A 401 in the panel message means the key is wrong or lacks the campaign permission; a 400 naming the sender means the address is not verified.
