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.
Three forces drove the change:
| 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 |
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
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
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
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.