24-Jun-2026 — Upgrade Brevo SDK to v5, add a subscription audit trail with Salesforce consent writeback #168

build 720commit ce096af


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.

Brevo SDK v5 and the Contact Import migration

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.

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.

Incremental and real-time member sync

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.

Subscription changes are now audited at the point of save

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.

Salesforce consent writeback

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 and Brevo list membership

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.

Brevo daily send allowance now comes from the account

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.

Reliability fixes

Mail-admin UX

Why now

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.

How to try it out

Against a real Brevo account on a deployed environment:

  1. Run a member list update from the admin Brevo tools and confirm contacts appear in the expected lists with their attributes.
  2. Edit a member in the member editor and confirm the change reaches Brevo without sending an email; edit contact preferences as a member and confirm the same.
  3. Unsubscribe a member from their last list (from the member admin modal or the self-service email-subscriptions page) and confirm, together: the local "Unsubscribed from list" audit, the Brevo contact deleted, and a "Salesforce consent writeback succeeded" audit.
  4. Open the email composer and start a send when nothing has changed; confirm the pre-send sync reports it is already in sync and does not pull the whole contact list.
  5. Send a test transactional email and a test campaign.
  6. Open the unsubscribe and blocked-contact views and confirm they load; confirm the sender and domain management screens list and update correctly.

Technical changes

Verification

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.