Skip to main content

Magento 2 Brevo Integration for Blocking Transactional Email to Blacklisted Contacts

10 min read May 15, 2026
Magento Brevo Email Integrations

Introduction

This article walks through a Magento 2 module that checks Brevo contact blacklist status before Magento sends selected sales emails.

The module is available on GitHub at beljic/module-brevo. It does not replace Magento's email transport and it does not send email through Brevo. Magento still sends email through its normal configured path. Brevo is used only as a lookup service for one decision:

Should Magento send this order, invoice, or shipment email to this customer address?

The integration is intentionally small:

  • read Brevo API credentials from Magento config
  • call Brevo Contacts API for a customer email address
  • cache the blacklist result
  • block Magento sales email senders when Brevo confirms the address is blacklisted
  • optionally render a frontend status indicator for logged-in customers

That scope is useful because it keeps the module away from template rendering, SMTP configuration, message queues, and provider-specific sending logic.

What the Module Does

The module checks whether a customer email address is blacklisted in Brevo and then uses that status in two places.

First, it blocks selected Magento transactional emails:

  • order confirmation emails
  • invoice emails
  • shipment emails

Second, it can show a small status indicator in the storefront header for logged-in customers.

The module is configured through Magento Admin under Stores > Configuration > Brevo > API, with fields for:

  • enabling or disabling the integration
  • storing the Brevo API key
  • setting the cache lifetime for blacklist responses

That gives the integration a deliberately narrow responsibility. Magento still owns order lifecycle and email rendering. Brevo remains the source of truth for contact blacklist status. The module sits between them and decides whether a specific recipient should receive a message.

Architecture Overview

At a high level, the module has four layers.

Magento email sender
  -> around plugin
  -> Brevo service
  -> Brevo client
  -> Brevo Contacts API

The important classes are:

  • Beljic\Brevo\Helper\Data
  • Beljic\Brevo\Model\BrevoClient
  • Beljic\Brevo\Service\BrevoService
  • Beljic\Brevo\Plugin\Email\PreventBlacklistedOrderSender
  • Beljic\Brevo\Plugin\Email\PreventBlacklistedInvoiceSender
  • Beljic\Brevo\Plugin\Email\PreventBlacklistedShipmentSender
  • Beljic\Brevo\ViewModel\CustomerStatus

The dependency injection configuration wires the service and client interfaces to concrete implementations, and attaches plugins to Magento's sales email senders.

<type name="Magento\Sales\Model\Order\Email\Sender\OrderSender">
    <plugin name="prevent_blacklisted_order_sender_plugin"
            type="Beljic\Brevo\Plugin\Email\PreventBlacklistedOrderSender"
            sortOrder="10"/>
</type>

The same pattern is used for invoice and shipment email senders. The plugin does not rewrite templates, intercept SMTP, or modify transport logic globally. It guards the specific sender class where Magento is already about to dispatch the message.

Configuration Boundary

Magento configuration is handled by Beljic\Brevo\Helper\Data.

The helper reads three values:

  • brevo_api/settings/enabled
  • brevo_api/settings/api_key
  • brevo_api/settings/cache_ttl

The default configuration keeps the module disabled:

<default>
    <brevo_api>
        <settings>
            <enabled>0</enabled>
            <api_key/>
            <cache_ttl>3600</cache_ttl>
        </settings>
    </brevo_api>
</default>

That default matters. A module that can block transactional emails should not become active just because it was installed. The merchant or developer should explicitly enable it after adding the API key and validating the behavior in staging.

The cache TTL is also a useful admin-controlled setting. Blacklist status is external state, but it is not usually something that must be checked on every page load or every email send. Caching avoids unnecessary Brevo API calls and protects checkout/order workflows from avoidable latency.

Calling Brevo

The Brevo-specific API logic lives in Beljic\Brevo\Model\BrevoClient.

The flow is straightforward:

  1. Hash the email address into a cache key.
  2. Return the cached blacklist result if it exists.
  3. Create a Brevo API configuration with the API key.
  4. Call getContactInfo($email) on Brevo's Contacts API.
  5. Read getEmailBlackListed() from the response.
  6. Cache the result for the configured TTL.

The cache key uses a SHA-256 hash of the email address:

$cacheKey = 'brevo_blacklist_' . hash('sha256', $email);

That is better than placing raw email addresses directly into cache keys. Cache keys leak into logs, debugging output, Redis tools, and operational scripts more often than people expect.

The API call itself is intentionally isolated:

$config = Configuration::getDefaultConfiguration()
    ->setApiKey('api-key', $this->configHelper->getApiKey());

$api = new ContactsApi(null, $config);
$response = $api->getContactInfo($email);

return $response?->getEmailBlackListed();

The module catches both Brevo ApiException and generic Throwable, logs the error, and returns null.

The failure behavior needs to be explicit. If Brevo is temporarily unavailable, Magento has two options: send the email anyway or block it because suppression status cannot be confirmed.

For sales email, I would normally use fail-open behavior. If the Brevo lookup fails, send the order confirmation instead of silently suppressing a critical customer message. Marketing email can use stricter rules, but order, invoice, and shipment email should not disappear just because an external API timed out.

The current code mostly behaves that way through the dedicated plugins, because null is not truthy and therefore does not trigger the block condition.

Blocking Magento Sales Emails

The email blocking logic is implemented with around plugins.

For order confirmations, the plugin wraps Magento\Sales\Model\Order\Email\Sender\OrderSender::send():

public function aroundSend(
    OrderSender $subject,
    callable $proceed,
    Order $order,
    bool $forceSyncMode = false
): bool {
    $email = $order->getCustomerEmail();

    if ($email && $this->brevoApi->isBlacklisted($email)) {
        $this->logger->debug('Order email send blocked for: ' . $email);
        return false;
    }

    return $proceed($order, $forceSyncMode);
}

Invoice and shipment emails follow the same idea. The plugin extracts the order email from the invoice or shipment, checks Brevo, and returns false instead of calling $proceed() when the address is blacklisted.

This keeps suppression close to the send event. The module does not need an observer that guesses which email is about to be sent. It does not need to modify sales entities. It does not need to disable all email delivery globally.

It only says: before this sales email sender runs, check this recipient.

Storefront Status Indicator

The module also adds a frontend block into header.container through view/frontend/layout/default.xml.

<referenceContainer name="header.container">
    <block class="Magento\Framework\View\Element\Template"
           name="customer.brevo.status"
           template="Beljic_Brevo::customer/status.phtml"
           before="-">
        <arguments>
            <argument name="viewModel" xsi:type="object">Beljic\Brevo\ViewModel\CustomerStatus</argument>
        </arguments>
    </block>
</referenceContainer>

The view model checks:

  • whether the module is enabled
  • whether the customer is logged in
  • whether the logged-in customer's email is blacklisted

The template then renders a simple status:

<?= $viewModel->isBlacklisted() ? '&#10060; Email blacklisted' : '&#9989; Email OK'; ?>

This is useful during development and QA because the Brevo lookup becomes visible without checking logs. A tester can log in as a known blacklisted contact and confirm that Magento resolves the expected status.

For production, I would not render this globally in the header. Customers generally do not need to see "Email blacklisted" in storefront chrome. The status is more appropriate for QA, support tooling, a customer account diagnostics area, or an admin customer view.

Implementation Notes

The module has a narrow boundary.

It does not try to become a full email platform integration. It does not replace Magento's sender pipeline, template rendering, or SMTP transport. It only checks one external status and uses that status where it matters.

The use of small interfaces is also helpful:

  • BrevoClientInterface owns provider communication
  • BrevoServiceInterface owns application-level access
  • email plugins depend on the service, not directly on API setup

That makes the module easier to test. Unit tests can fake the service and assert that Magento's send method is skipped only when the Brevo status says the email is blacklisted.

Caching is another practical decision. Calling Brevo for every order, invoice, shipment, and page render would be wasteful. A one-hour TTL is a reasonable default for a status that rarely changes minute by minute.

Suggestions I Would Make Before Production Use

Before using this module in production, I would make these changes.

First, the API key field should use an encrypted backend model. Right now the config field is a plain text field. In Magento, API secrets should be stored with Magento\Config\Model\Config\Backend\Encrypted and rendered as an obscured password field where appropriate.

Second, the frontend template calls isBlacklisted() multiple times. Because the result is cached this is not disastrous, but it still makes the template do more work than necessary. Resolve the value once:

$isBlacklisted = $viewModel->isBlacklisted();

if ($isBlacklisted === null) {
    return;
}

Then render from that local variable.

Third, normalize email addresses before hashing and lookup. At minimum, trim and lowercase the address. That avoids separate cache entries for equivalent values such as Customer@example.com and customer@example.com.

Fourth, make fail-open behavior explicit. The API client currently returns null on API failures while the interfaces declare bool. That mismatch should be cleaned up. Either return a strict boolean and document the fallback, or update the interface to ?bool and make plugin behavior deliberate:

$isBlacklisted = $this->brevoApi->isBlacklisted($email);

if ($isBlacklisted === true) {
    return false;
}

That says exactly what the business rule is: only block when Brevo positively confirms the blacklist status.

Fifth, clean up the unused combined plugin class. The repository has dedicated order, invoice, and shipment plugins wired in di.xml, plus a separate PreventBlacklistedSender class that is not referenced by the current DI configuration. Keeping only one approach reduces confusion.

Sixth, avoid logging raw email addresses at debug level in production. For support diagnostics it is useful to know that a message was blocked, but raw customer emails in logs create unnecessary personal data exposure. A hashed email, order ID, and message type are usually enough.

Seventh, add a cache invalidation strategy. If support removes a contact from the Brevo blacklist, Magento may still use the cached status until the TTL expires. That may be acceptable, but support teams should understand the delay or have a way to clear the specific cache entry.

Testing Strategy

The module already includes unit tests for the Brevo client, service-facing behavior, and customer status view model.

The tests that matter most for this integration are:

  • blacklisted order email is not sent
  • non-blacklisted order email proceeds
  • Brevo API failure does not block critical transactional email
  • cached blacklist values avoid repeated API calls
  • disabled module does not render frontend status
  • logged-out customer does not trigger a Brevo lookup

For integration testing, I would add Magento-level tests around the three plugin targets. Around plugins are easy to get wrong because method signatures must match Magento core. A minor Magento upgrade can change a sender class or optional parameter behavior. Tests around the plugin boundary catch that sooner.

I would also test cache behavior with normalized emails. That is especially important if email values can come from guest checkout, admin order creation, marketplace imports, or customer account data.

Where This Pattern Fits

This pattern fits stores where Brevo already owns contact suppression state and Magento needs to respect that state before sending sales emails.

It is especially relevant for stores where:

  • support teams manage contact status in Brevo
  • deliverability rules live outside Magento
  • transactional email should avoid known blocked recipients
  • order, invoice, and shipment emails need provider-aware suppression
  • developers want to avoid replacing Magento's entire email pipeline

It is not a Brevo transactional email transport. It does not send Magento templates through Brevo's SMTP or template API. That would be a separate module. This one uses Brevo as a decision service before Magento sends selected emails through its existing path.

Summary

This Brevo integration is not a Magento email transport module. It is a suppression guard.

The useful parts are:

  • Brevo owns blacklist status
  • Magento owns order, invoice, and shipment email sending
  • the module reads one piece of provider state
  • the result is cached
  • around plugins apply the decision at the sales email sender boundary

The module enforces one business rule without replacing Magento's email pipeline.