14-Jun-2026 — Brevo Member Sync: How the Information Flow Changes #168


This article describes how member and consent information moves between NGX Ramblers, Brevo and Salesforce, and how that flow changes with the Brevo SDK v5 upgrade and the move to incremental member synchronisation. It is a companion to the email architecture article and focuses on one question: when a member changes, what travels where, and when.

Why this changed

Three forces drove the change:

  1. Brevo retires the Bulk Contact Update endpoint on 30 October 2026. The member-to-Brevo list sync relied on that endpoint, so it had to move to the Contact Import API.
  2. The sync was brute-force. Before sending any email, the platform pulled the entire Brevo contact list and reconciled it from scratch, on every send, whether or not anything had changed.
  3. Consent did not reach Brevo. A member whose marketing consent was withdrawn was excluded from NGX-composed sends, but stayed on the Brevo lists, so the two systems could disagree.

At a glance

Aspect Before After
Brevo SDK @getbrevo/brevo v2.0.0-beta.4 (pre-release, deprecated request library) v5.0.4 (single BrevoClient, native fetch, typed errors)
Bulk list update updateBatchContacts (POST /v3/contacts/batch, retiring 30 Oct 2026) Contact Import API (importContacts)
Pre-send sync Full contact-list pull and reconcile on every send No-op when nothing changed since the last sync
Member edits Reach Brevo only on the next send Pushed to Brevo immediately on save
Consent withdrawal Excluded from NGX sends only; member stayed on Brevo lists Removes the member from Brevo lists as well
Salesforce consent Stored locally, never propagated to Brevo Withdrawal propagates through to Brevo list removal

The information flow before

Member changes only reached Brevo when someone sent an email. At that point the platform read the whole Brevo contact list, compared it against the membership, and pushed the differences back using the bulk update endpoint. Consent lived on the member record but only affected who NGX chose to email; it never changed Brevo list membership.

flowchart LR
    ADMIN@{ icon: "ngx:user", label: "Group admin", pos: "b", h: 48 }
    MEMBERS@{ icon: "ngx:mongodb", label: "Member records", pos: "b", h: 48 }
    ADMIN -->|"edit member"| MEMBERS

    subgraph ngx["NGX Ramblers"]
        SEND["Send an email"]
        RECON["Full reconcile<br/>pull ALL contacts, diff, push"]
        SEND --> RECON
    end

    MEMBERS -.->|"only at next send"| SEND
    RECON -->|"updateBatchContacts<br/>POST /v3/contacts/batch"| BREVO@{ icon: "ngx:brevo", label: "Brevo lists", pos: "b", h: 48 }
    RECON -->|"reads entire list every time"| BREVO

    CONSENT["Consent withheld"]
    MEMBERS --- CONSENT
    CONSENT -.->|"excluded from NGX send only"| SEND

    style ngx fill:#FBE9E7,stroke:#E0A89B,stroke-width:2px,rx:12,ry:12,color:#404143

The information flow after

Each member now carries a small signature of exactly the fields that go to Brevo (email, names, member attributes, the lists they are eligible for, and whether marketing consent is withheld). That signature is the pivot for the whole flow:

flowchart LR
    ADMIN@{ icon: "ngx:user", label: "Group admin / member", pos: "b", h: 48 }
    MEMBERS@{ icon: "ngx:mongodb", label: "Member records<br/>+ sync signature", pos: "b", h: 48 }
    ADMIN -->|"edit member / preferences"| MEMBERS

    subgraph ngx["NGX Ramblers"]
        RT["Real-time push<br/>changed member only"]
        CHECK["Pre-send check<br/>signature dirty?"]
        SEED["Seed / periodic reconcile"]
    end

    MEMBERS -->|"on save"| RT
    MEMBERS -->|"on send"| CHECK
    CHECK -->|"nothing changed"| NOOP["No Brevo call"]
    CHECK -->|"changed members only"| RT

    RT -->|"create / update / add / remove"| BREVO@{ icon: "ngx:brevo", label: "Brevo lists", pos: "b", h: 48 }
    SEED -->|"importContacts<br/>Contact Import API"| BREVO

    CONSENT["Consent withheld<br/>Head Office or group"]
    MEMBERS --- CONSENT
    CONSENT -->|"remove from lists"| RT

    style ngx fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

The three directions of consent and membership

It helps to separate the directions information now travels.

Outbound — NGX to Brevo (membership and attributes). Member edits flow to Brevo on save and, as a backstop, at send time and via the periodic reconcile. New and changed members are created or updated; members who become ineligible (unsubscribed or consent withheld) are removed.

Inbound — Brevo to NGX, then NGX to Salesforce (unsubscribes). When a contact unsubscribes or is blocked at Brevo, that comes back through the Brevo webhook and the scheduled blocklist sync, which records the block on the member and flips their subscriptions. If that takes the member to a full opt-out, NGX writes the consent change back to Salesforce. This direction is unchanged by this work, but it now sits alongside the outbound consent path below.

New — Salesforce to Brevo (consent). Previously a consent withdrawal arriving from Salesforce changed only the local member record. Now, because consent is part of Brevo eligibility, that withdrawal removes the member from their Brevo lists on the next sync.

flowchart TB
    SF@{ icon: "ngx:ramblers-hq", label: "Salesforce / Central Office", pos: "b", h: 48 }
    MEMBERS@{ icon: "ngx:mongodb", label: "NGX member records", pos: "b", h: 48 }
    BREVO@{ icon: "ngx:brevo", label: "Brevo", pos: "b", h: 48 }
    MEM@{ icon: "ngx:user", label: "Member inbox", pos: "b", h: 48 }

    SF -->|"member + consent import"| MEMBERS
    MEMBERS -->|"full opt-out writeback"| SF
    MEMBERS -->|"membership, attributes and consent<br/>on save / send / reconcile"| BREVO
    BREVO -->|"unsubscribe / block webhook"| MEMBERS
    BREVO -->|"newsletters and notifications"| MEM

How consent is interpreted

Consent suppression mirrors the rule already used at send time, so Brevo membership and the recipient list now agree.

Mode Setting Gate that removes a member from Brevo
Insight Hub parity (default) granular consent off Head Office consent: emailMarketingConsent is false and respectHeadOfficeConsent is on
Granular consent granular consent on Group-level consent: groupMarketingConsent is false

Head Office consent represents consent to receive Central Office marketing, which is not the same as consent to hear from a member's own group. That is why some groups turn respectHeadOfficeConsent off, and why, once granular consent is available, group-level consent takes over as the gate. The area and other granular flags are not used for a group's own site. The decision is driven by the member's own consent fields, so it does not depend on a member-facing page reading the administrator-only Salesforce configuration.

What stays the same

Operational notes