Résultat
duplicate contacts merged
Résultat
duplicate companies merged
Résultat
contacts migrated from Sarbacane to HubSpot, then Brevo
● Context
The client: TGV Lyria, high-speed rail between France and Switzerland
TGV Lyria operates high-speed trains between France and Switzerland. Behind every Paris–Geneva or Paris–Zurich connection, a small communications team runs high-volume campaigns across three markets and three languages, France, French-speaking Switzerland, and German-speaking Switzerland, under the strict process constraints of two major rail groups: SNCF and CFF.
When the time came to change emailing platform, "good enough" wasn't an option. A strategic commercial campaign was already booked in the calendar.
The problem: a CRM migration that couldn't afford a single misstep
When we kicked off the project, the situation was textbook "fast and reliable, you have to pick both":
- A migration on a deadline. Move from Sarbacane to Brevo without losing data quality, deliverability, or usability with a major commercial push already scheduled.
- Three tools, none of them talking cleanly. Sarbacane for legacy sending, HubSpot as the CRM core, Brevo planned as the new transactional and marketing platform. No consistent contact view across the three.
- Years of messy data. Source data lived in flat CSV/Excel files split by language. Opt-ins stored as
"Oui","Ja","1"and a dozen other variants. Complaints, NPAI (postal returns), unsubscribes and bounces all mixed back into the same lists as active contacts. - Two large-group compliance layers. SNCF and CFF brought their own cybersecurity, GDPR, and internal validation workflows on top of standard B2C marketing operations.
The risks were just as clear: lose the ability to send mass communications, degrade domain reputation through bad-quality data, or end up with a workflow heavier than the one we were replacing.
The ask: rebuild the data and email stack on HubSpot + Brevo, clean years of legacy noise, and ship before the next campaign window, without interrupting a single send.
● The Cashmyrr solution
Why HubSpot + Brevo (and not Sarbacane v2)
Sarbacane handled the sending well enough, but it didn't solve the underlying problem: TGV Lyria's CRM was a collection of imports, not a database. We needed two things at once:
- A CRM core that holds a clean, structured contact model, with native segmentation, lifecycle stages, parent/child company relationships, and full GDPR traceability. HubSpot.
- A bulk-sending platform with strong deliverability tooling, native authentication (DKIM/SPF/DMARC), IP warming, and bi-directional sync with the CRM. Brevo.
Concretely, this combination let us ship three things Sarbacane alone never could:
- A single source of truth for all three markets, with opt-ins, lifecycle stages, and language fields aligned.
- A dedicated sending subdomain (
newsletter.lyria.com) that protects the main domain reputation regardless of campaign volume. - Bi-directional sync between Brevo (sends, bounces, unsubscribes) and HubSpot (the source of truth) every ~6 hours, with workflow-based suppression to keep deliverability high.
● Steps
The architecture in production
The new stack has four layers:
- A staging database (SQL + MongoDB) where raw CSV/Excel files are normalised before they ever touch HubSpot.
- An n8n ingestion workflow that does smart upserts into HubSpot, checking what already exists, updating only missing fields, never overwriting good data with worse data.
- HubSpot as the structured source of truth, with B2B parent/child accounts, lifecycle stages, and source-based auto-segmentation.
- Brevo on a dedicated subdomain for bulk sending, with native bi-directional sync back to HubSpot.
The only custom layers are the staging pipeline and the n8n ingestion controller. Everything downstream is native HubSpot and Brevo.
The migration, step by step
Step 1 - From flat files to a controlled staging area
Importing legacy CSVs straight into HubSpot would have baked years of noise into the new system. Instead, we routed everything through a SQL + MongoDB staging layer first.
MongoDB aggregation pipelines ($project, $unionWith, $lookup) handled the heavy lifting:
- Normalisation: opt-ins collapsed to a single boolean, dates standardised, fields aligned across FR/DE/EN sources.
- Dynamic exclusion: emails flagged in
complaintsandnpaicollections were filtered out before reaching HubSpot. - Format scrubbing: phone-number-as-email entries (
0606060606@icloud.com) and other deliverability-killing addresses removed at this stage.
Only data that passed staging made it downstream.
Step 2 - Smart upserts into HubSpot
We didn't trust an "Import All" button with a CRM this loaded. We built an n8n workflow as the ingestion controller:
- The workflow queries HubSpot in batches via API.
- For each email, it checks whether the contact already exists.
- It then updates only missing fields or creates a new contact — never overwriting better data with worse.
The result: TGV Lyria ended up with an enriched CRM, not an overwritten one.
Step 3 - A dedicated subdomain + full authentication
Bulk campaigns were re-anchored on infrastructure that protects the main domain:
- Dedicated subdomain:
newsletter.lyria.com, separated from transactional and corporate mail. - Full authentication stack: DKIM, SPF, DMARC configured for maximum inbox placement.
- Bi-directional sync: native Brevo ↔ HubSpot sync every ~6 hours, so hard bounces and unsubscribes from Brevo land back in HubSpot automatically.
- Suppression workflows in Brevo that remove hard and soft bounces ahead of every first send.
Step 4 - Cleaning the legacy database with clustering + AI
Years of imports had created a fuzzy-duplicate problem that simple email-based dedup couldn't solve:
jean.dupont@company.comvsj.dupont@company.fr- Typos and alternate spellings on first/last names
- B2B/B2C profiles mixed together
We layered three stages:
1. Deterministic clustering - grouping potential duplicates by three keys: exact email, domain + last name, phone + last name. This produced focused clusters without merging anything yet.
2. AI-assisted scoring on the grey zone - for pairs with initial similarity between 0.60 and 0.84, we used GPT to assess semantic similarity: nickname vs full name, typical typos, swapped first/last name, abbreviated names. GPT returned a confidence score, and only pairs at ≥ 0.85 were merged automatically. Everything below stayed in a manual review queue.
3. Smart merge rules - field by field, never destructive:
- Keep the most valid email.
- Keep the most advanced lifecycle stage (Customer > Opportunity > Lead).
- Preserve the existing owner where it made sense.
Step 5 - A B2B parent/child account model
For B2B data, we restructured around a parent/child company model:
- Companies sharing the same
root_domain(e.g.hermes.com) were identified. - A script then designated a parent company (typically the head office) and linked local entities as child companies.
Sales got a global view of key accounts without losing the local contact context.
Step 6 - Fixing the data at the source
Cleaning legacy data is one thing. Stopping new pollution is what makes it sustainable.
Several "home-made" inputs were injecting low-quality contacts directly into HubSpot - pure HTML forms, search bars, and other on-site features that triggered automatic contact creation. The result: thousands of "Organic Leads" with no usable info and no consent. Marketing cookies were also being set before user opt-in, raising GDPR concerns.
The most severe pain point was structural: HubSpot was treating every input field as a lead source. Search queries, portal logins, contact form submissions, all created new contacts and companies in the CRM.
We fixed it in two moves:
1. API lockdown. Disabled HubSpot's "Non-HubSpot Forms" capture. Only intentional, controlled touchpoints can now create contacts.
2. Conditional tracking via CookieFirst. Rewrote the GTM integration so the HubSpot tracking script and the hubspotutk cookie only fire after explicit consent through the CookieFirst CMP.
On top of that, every new contact is auto-segmented by source and intent through HubSpot workflows: a contact from the newsletter form is tagged as a B2C target; a contact from Salesforce or with a specific Owner is tagged as B2B. Reporting and campaign relevance start clean from contact creation.
The guardrails: documentation as a deliverable
A project distributed across n8n workflows, Python/SQL scripts, MongoDB pipelines and HubSpot configuration has one critical failure mode: knowledge living only in people's heads.
We treated documentation as a first-class deliverable from day one:
- Technical documentation in Markdown describing the what (architecture, workflows, scripts) and the how, with JSON snippets for key n8n workflows, MongoDB queries, and their underlying logic. Any developer joining later can understand the system without reverse-engineering it.
- Maintenance runbooks, step-by-step procedures for Brevo sync log checks, handling new duplicates, and other recurring operations. Not theoretical notes, actual day-to-day playbooks.
- Decision traceability, merge rules and AI thresholds documented in plain language. If a contact merge is ever challenged, the underlying logic is auditable.
● Result
What shipped, the output in production
Zero campaign interruption. The full migration shipped without skipping a single send, despite the strategic commercial push already on the calendar.
A structured foundation across three markets and languages, ready for high-volume campaigns on Brevo, with opt-ins, lifecycle stages, and language fields aligned across FR / FR-CH / DE-CH.
A protected email infrastructure, dedicated subdomain, full DKIM/SPF/DMARC, native bi-directional sync with HubSpot. Domain reputation isolated from campaign volume.
Dramatically lower data pollution risk, thanks to staging, deterministic + AI deduplication, and source-level lockdown of uncontrolled web inputs.
A B2B account model that surfaces parent/child company relationships and supports key-account views.
Clear documentation and runbooks that make internal teams operationally self-sufficient and resilient to staff changes.
Cashmyrr was able to propose an action plan that was effective, realistic, and aligned with our timing constraints, especially a strategic commercial campaign already in the calendar.
We benefited from a dual expertise: marketing and project leadership with Antoine, and deep technical support with Telmo.
Availability, expertise, and agility in projects. There is a real sense of building alongside the client.
— TGV Lyria
What we learned migrating high-volume B2C on HubSpot + Brevo
What worked
- Staging before HubSpot, every time. Treating HubSpot as the destination, not the processing engine, was the single decision that made the rest of the project tractable. Cleaning fields, normalising opt-ins and applying suppression lists in MongoDB is fast, cheap and auditable. Cleaning the same data inside HubSpot would have meant workflows on workflows, lower performance and higher cost.
- AI on the grey zone, not on everything. Deterministic clustering handled the majority of duplicates. GPT was only invoked for the ambiguous middle band (0.60–0.84 similarity). Predictable cost, predictable behaviour, and a single band of decisions to audit.
- n8n as the ingestion controller. Smart upserts via API beat any bulk import. We chose what to update, what to leave alone, and how to log every operation, none of which a default HubSpot import gives you.
- Documentation as a deliverable, not an afterthought. Markdown technical docs and operational runbooks made the handover boring, which is exactly what you want on a project this distributed.
The limits we hit
- HubSpot's default behaviours need active lockdown. Out of the box, HubSpot creates contacts from sources you didn't intend to be lead sources. Non-HubSpot Forms capture, the tracking script, the cookie set before consent, all of it required explicit configuration. If you don't address this on day one, your "clean" CRM dirties itself again within weeks.
- Native Brevo ↔ HubSpot sync runs every ~6 hours. Fine for marketing campaign cadence, slightly tight for fast feedback loops on send issues. We compensated with Brevo-side suppression workflows ahead of every first send.
- Multi-language sources don't normalise themselves. FR/DE/EN data came with their own opt-in vocabularies and date formats. Normalisation rules had to be explicit, language by language, in the staging layer, never assumed.
● FAQ
Don't import legacy CSVs directly. Route every source file through a staging database first (we use SQL + MongoDB) to normalise opt-ins, exclude complaints and NPAI, and scrub bad formats. Only data that passes staging reaches the new CRM. In parallel, configure Brevo with a dedicated sending subdomain and full DKIM / SPF / DMARC authentication before the first send, so deliverability doesn't tank on legacy noise.
Yes, if the cutover is staged. We migrated TGV Lyria's stack across three markets and three languages without skipping a single send, including during a strategic commercial campaign already on the calendar. The principle: clean and validate data in a staging layer, set up the new sending infrastructure in parallel, and only flip campaigns once Brevo is fully authenticated and synced with HubSpot.
Because the two jobs are different. HubSpot is a CRM core: structured contact model, lifecycle stages, parent/child company relationships, segmentation, GDPR traceability. Brevo is a bulk-sending platform: deliverability tooling, IP warming, native authentication. All-in-one tools tend to be strong on one side and weak on the other. The combination, with native bi-directional sync, gives you both without compromise.