Blog Open the app

How to Deduplicate a Customer List Before Uploading to Facebook, Google & LinkedIn Ads

July 30, 2026 · 11 min read · Written by Sam Kale

Every paid media team sits on a first-party data goldmine: a list of paying customers, trial users, or high-intent leads that gets uploaded to Meta Custom Audiences, Google Customer Match, and LinkedIn Matched Audiences to run retargeting, suppression, and lookalike campaigns.

And almost every one of those lists is dirty. The typical export from a CRM plus billing system plus marketing tool has a 10-25% duplicate rate. That duplicate rate quietly costs money in three ways at once: it inflates the audience size Meta charges you against, it drags down your reported match rate, and it distorts the seed data your lookalike model is built on.

This is the pre-upload workflow that fixes it in 15 minutes. It works whether you're uploading to Meta Ads Manager, Google Ads Customer Match, LinkedIn Campaign Manager, TikTok, or all four.

What's in this guide

  1. Why audience duplicates cost real money
  2. What each platform does (and doesn't) dedupe for you
  3. Where duplicates come from in a customer list
  4. The 5-step pre-upload cleanup workflow
  5. Step 2: Normalize before hashing
  6. Step 3: Fuzzy dedup on cleartext
  7. Step 4: Hash and upload
  8. Special case: cleaning lookalike seed lists
  9. FAQ

Why Audience Duplicates Cost Real Money

Every ad platform bills against the audience that actually gets served impressions, not against the file you uploaded. So at first glance, duplicates in the upload look free. They aren't. Here's the actual damage:

Rule of thumb from our audits: for every 10 percentage points of duplicates you remove from an audience file, expect a 3-5 point reported match-rate lift and a 2-4% reduction in wasted retargeting spend on that campaign. On a $50k/month retargeting budget, that pays for a lot of good coffee.

What Each Platform Does (And Doesn't) Dedupe for You

PlatformDedupes exact hashes?Dedupes across identifiers?Normalization before hash?
Meta (Facebook / Instagram) Custom AudiencesYesNoYou must normalize first
Google Ads Customer MatchYesNoYes (Google normalizes email & phone before hashing if you use the API)
LinkedIn Matched AudiencesYesNoYou must normalize first
TikTok Custom AudiencesYesNoYou must normalize first
X (Twitter) Tailored AudiencesYesNoYou must normalize first

Two things to notice:

  1. No platform dedupes across identifiers. The same person with two email addresses is two audience members everywhere. Only you can catch that.
  2. Only Google normalizes for you, and only on the API path (and only for email and phone). Everywhere else, "bob@acme.com" and "Bob@Acme.com" hash to two totally different values.

Both problems are solved by doing the dedup and normalization on your side before you upload. That's what the rest of this guide is about.

Where Duplicates Come From in a Customer List

Before you clean, know what you're cleaning. Every messy audience export we've seen is some mix of these:

The best long-term fix is upstream: clean the CRM. The short-term fix, which you should also do every time, is a pre-upload pass on the audience file itself.

The 5-Step Pre-Upload Cleanup Workflow

  1. Export a superset audience CSV with all identifiers you have.
  2. Normalize emails, phones, and names on cleartext.
  3. Fuzzy dedupe to collapse the same person across identifiers.
  4. Hash the survivors with SHA-256.
  5. Upload to Meta, Google, LinkedIn (or all three).

The critical bit that most teams get wrong: do all the normalization and dedup on cleartext, then hash as the very last step. Hashing is one-way. Once you've hashed, "bob@acme.com" and "Bob@Acme.com" are two different rows forever.

Step 2: Normalize Before Hashing

Meta, LinkedIn, TikTok, and X all require you to normalize before hashing (they'll accept unnormalized input but your match rate craters). Google normalizes for you on the API path but not on the manual CSV upload. Safe rule: always normalize, always. Do these transformations on every identifier column:

Email

Phone

Name

City / State / Country

A minimal Python normalization snippet:

import re

def norm_email(e):
    e = (e or "").strip().lower()
    if not e or "@" not in e: return ""
    local, domain = e.split("@", 1)
    if domain in ("gmail.com", "googlemail.com"):
        local = local.split("+")[0].replace(".", "")
    return f"{local}@{domain}"

def norm_phone(p, default_cc="1"):
    digits = re.sub(r"\D", "", p or "")
    if not digits: return ""
    if len(digits) == 10: digits = default_cc + digits
    return "+" + digits

def norm_name(n):
    n = re.sub(r"\s+", " ", (n or "").strip().lower())
    return re.sub(r"[^\w\s]", "", n)

Step 3: Fuzzy Dedupe on Cleartext

Normalization only catches "same identifier, different formatting" duplicates. To catch "same person, different identifier" you need a fuzzy pass. Weight your columns something like this:

ColumnWeightWhy
Normalized email35%Strongest single signal when present
Normalized phone25%Nearly as good; complements email
First + last name20%Weak alone; strong with location
City + state + country10%Disambiguates common names
Company domain (for B2B)10%Splits Bob at Acme vs Bob at Delta

For consumer audiences, drop the company-domain column and redistribute its 10% to name + address. For B2B audiences, boost it to 15% at the expense of city/state.

Auto-merge above 88. Manual-review band 75-88. Below 75, keep as separate records. See the reasoning for these numbers in Fuzzy Match on Multiple Columns and the thresholds section of our dedup guide.

Survivor rule for audience files: pick the row with the most non-empty identifiers. More identifiers per row = higher chance of matching on the platform. If two rows are tied, keep the one with the most recent last-purchase or last-engagement date.

Any of these three approaches works:

Step 4: Hash and Upload

Once you have survivors.csv, hash the identifier columns with SHA-256 (Meta, LinkedIn, TikTok, X, Google all use the same algorithm). Save the hashed file. Upload it. Discard the hashed file after upload — you don't want unhashed customer identifiers sitting around, and you don't want hashed ones sitting around either.

A minimal hashing snippet:

import hashlib, csv

def sha256(s):
    return hashlib.sha256((s or "").encode("utf-8")).hexdigest()

with open("survivors.csv") as fin, open("hashed.csv", "w", newline="") as fout:
    r = csv.DictReader(fin)
    w = csv.DictWriter(fout, fieldnames=["email","phone","fn","ln","ct","st","country"])
    w.writeheader()
    for row in r:
        w.writerow({
            "email":  sha256(row["email"]),
            "phone":  sha256(row["phone"]),
            "fn":     sha256(row["first_name"]),
            "ln":     sha256(row["last_name"]),
            "ct":     sha256(row["city"]),
            "st":     sha256(row["state"]),
            "country":sha256(row["country"]),
        })

Column-header conventions differ per platform:

Grab the current template from each Ads Manager (they change occasionally) and map your survivors columns to it before uploading.

Special Case: Cleaning Lookalike Seed Lists

Lookalikes are where a dirty seed hurts you the most, because the algorithm is optimizing to look like whatever's in the seed — including the duplicates.

Extra rules on top of the standard workflow:

  1. Dedupe first, then filter to your best segment. If you dedupe after filtering, you'll accidentally over-filter (dropping duplicates of high-value customers throws out both copies).
  2. Aim for a seed of at least 1,000 unique people for Meta/TikTok, 500 for Google, 300 for LinkedIn. Dupe removal cuts real seed size — you may need to broaden your definition of "best customer" to compensate.
  3. Refresh the seed monthly. Lookalikes go stale. Cleanup + refresh is a monthly ritual for any team spending >$10k/month on prospecting.
  4. Never seed a lookalike on a suppression list. Sounds obvious. Happens all the time when someone grabs "everyone in the CRM" as the seed and forgets to exclude churned customers.

FAQ

How often should I run this pre-upload cleanup?

Every time you upload a fresh audience. Retargeting and Customer Match audiences typically get refreshed weekly or monthly — run the pipeline every refresh. Suppression audiences typically get refreshed daily via the API — wire the normalize + dedup pipeline into that job.

Can't I just use the CRM's export to Meta/Google integration?

You can, and it's convenient, but those integrations don't dedupe. HubSpot → Meta will push every duplicate contact in HubSpot straight into your Custom Audience. Salesforce → Google Ads Customer Match will push every duplicate account. Upstream cleanup (see the Salesforce and HubSpot guides) is the real fix. Pre-upload dedup is the belt-and-suspenders backup.

Does removing duplicates violate any platform policy?

No. Meta, Google, LinkedIn, TikTok, and X all explicitly recommend deduplication in their Customer Match / Custom Audience documentation. What they prohibit is uploading data you don't have consent for — that's an entirely separate compliance issue.

What if my source data has 30-40% duplicates — is the CRM the real problem?

Yes. A 30%+ duplicate rate on the export means the source is broken. Pre-upload dedup will patch the audience for today, but you'll be doing the same work every week. Fix the CRM first (see the Ultimate Guide to Data Deduplication) and then the export cleanup shrinks to a 5-minute sanity pass.

Should I dedupe within a single platform's audience file, or across all my audience files?

Within each file at minimum. Across files if you're running exclusion logic (e.g., "everyone in Audience A but not in Audience B") — otherwise the same person can end up in A twice under different identifiers, escape the exclusion, and get served both campaigns.

Wrapping Up

Pre-upload audience cleanup is the highest-leverage 15 minutes in paid media. It doesn't require a data engineer, it doesn't require new budget, and it directly improves match rate, retargeting efficiency, suppression accuracy, and lookalike quality at the same time.

Bake the workflow — export, normalize, fuzzy-dedupe, hash, upload — into every audience refresh. Do it in Python if your team already scripts, in a fuzzy tool if they don't. Either way, stop uploading dirty audiences to platforms that don't dedupe them for you.

Related Reading

Skip the Python script for one-off audience refreshes. DedupFuzzy runs the exact normalize + weighted fuzzy pipeline in your browser — upload CSV, download survivors, hash, upload to Meta/Google/LinkedIn. Free for 500 rows, no signup.

Try DedupFuzzy Free