The Brevo email integration now runs on the current @getbrevo/brevo v5 SDK, and the member-to-Brevo-to-Salesforce data flow is now consistent: a user action makes the full change to every system at the moment it happens, every subscription change is audited, and a member who fully unsubscribes is removed from Brevo and has their consent written back to Salesforce in the same step. The full reconcile is demoted to a repair and backstop rather than the thing that completes changes.
The v5 SDK replaces the December 2023 pre-release beta (v2.0.0-beta.4) that depended on the unmaintained request HTTP library. In the same change the member-to-Brevo list synchronisation moves off the Bulk Contact Update endpoint, which Brevo retires on 30 October 2026, onto the Contact Import API.
request dependency is removed; v5 uses native fetch with no external dependencies.Member list updates still run from the admin Brevo tools. Behind the scenes each batch is now submitted as a Contact Import job rather than a single synchronous call. Brevo accepts the job and processes it shortly afterwards, so the request returns once the job is accepted rather than once every contact is written. Contacts are matched by email and updated in place, and new contacts are created if they do not yet exist, which is the same outcome the sync produced before. The duplicate-contact fallback path is unchanged.
The member-to-Brevo list sync is incremental. Each member carries a signature of the values pushed to Brevo (email, names, member attributes, Brevo-eligible list ids, and whether Head Office consent is withheld) plus the list ids last synced. The pre-send reconcile first checks whether any member in the audience differs from its signature; if none do, it returns without contacting Brevo. When a member is saved through the admin member editor or the contact-preferences page, only that member is pushed, and a Brevo failure there is logged and left for the next reconcile rather than blocking the save.
User-invoked actions now perform the full extent of the change. The real-time per-member sync deletes the Brevo contact when a member becomes ineligible for every list (their last subscription removed, or consent withheld), instead of only removing them from a list and leaving the contact for the reconcile to clean up. Members who leave some lists but keep others still get the lighter remove-from-list and update treatment. The full reconcile remains as the initial seed and the periodic repair, and stamps the signature on every member it syncs so subsequent sends can skip.
Every change to a member's mailing-list subscriptions is recorded as an audit entry, regardless of which mechanism made it, so the trail can no longer be bypassed by a code path that forgets to write one.
When a member's active subscribed-list count drops to zero, their consent is written back to Salesforce in the same save, and this fires from every page that can clear subscriptions (member admin and the self-service email-subscriptions page). This matters because a fully unsubscribed member receives no further emails and so can never use a branded-unsubscribe link again; if the writeback did not happen at that moment it could never happen, and the local, Brevo and Salesforce states would fall out of step with no way back into sync. The writeback is locked to the Brevo contact deletion above, both keyed off the same "subscriptions reached zero" event.
The writeback authenticates with the site's active group code (systemConfig.group.groupCode), not the first configured API token. Leftover tokens for other groups can sit in the Salesforce config without redirecting a writeback to the wrong tenant; if the active group has no token it fails with a clear "no token for group <code>" message instead of silently using another tenant. The writeback triggers on subscriptions reaching zero rather than on Brevo-eligibility, so a member whose ineligibility comes from withheld consent (a value that usually originated in Salesforce) is not written straight back.
Marketing-consent withdrawal removes a member from their Brevo lists, not just excludes them from NGX-composed sends as before. A member whose relevant marketing consent is withheld is treated as eligible for no Brevo lists, so the reconcile removes or deletes their contact and the real-time path removes them from their lists. In Insight Hub parity mode (the default, granular consent off) the rule is Head Office consent: emailMarketingConsent false when respectHeadOfficeConsent is on. Once granular consent is enabled, group-level consent takes over: groupMarketingConsent false withholds the member, because for a group's own site that is the consent that applies (per #209, Head Office consent represents Central Office marketing, not group marketing). The decision is driven by the member's own persisted consent fields rather than by reading the admin-only Salesforce config in the member-facing client, which avoids exposing the per-group API keys held in that config to ordinary logged-in members. The base memberSubscribed / memberSubscribedToAnyList / subscribedListIds helpers are left consent-agnostic so the member-admin "subscribed without consent" diagnostic filter still works.
The member modal's "Subscriptions do not guarantee delivery" alert now reflects the actual send settings: the consent warning shows only when respectHeadOfficeConsent is on, and the block warning only when respectEmailBlocks is on, matching what a real send would do.
The manual "daily campaign limit" field is removed. The remaining daily campaign allowance is read directly from the Brevo account's sendLimit credits, the value Brevo actually reports and enforces. A paid plan with no daily hold reads as no limit. The separate "credits available out of 300 emails/day" panel is unchanged.
createOrUpdateAll) no longer crashes the server when one upsert fails; it returns a single response and logs the cause. A long-standing double-send was the trigger.Brevo notified us on 26 May 2026 that the Bulk Contact Update API is retired on 30 October 2026. Every deployed site runs this same code against its own Brevo account, so all sites would lose member list syncing after that date without this change.
Against a real Brevo account on a deployed environment:
request dependency.any with the real v5 SDK response types throughout. This corrected two shapes the old any had hidden: the segments handler wrapped each segment instead of returning the declared flat shape, and the domain DNS mapping read camelCase fields when v5 returns snake_case dns_records. Widened Account.marketingAutomation.key to optional in mail.model.ts.Server tsc clean, frontend type-check (tsc) clean, all 812 server unit tests pass (including new units for the subscription diff, the active-group token selection, the account-derived allowance and the full-opt-out trigger). Not yet exercised against a live Brevo or Salesforce account, and the frontend was not run through a full Angular build.