Data and reporting
Deduplicate the list before it reaches a platform
One customer, four rows: a marketplace order, a WhatsApp enquiry, a form fill, and a spreadsheet somebody kept. Collapsing them is ordinary data work — normalise, compare, keep one row. What is not ordinary is what happens next. The moment that list is uploaded to an ad platform it becomes a transfer of personal data, and every platform's own documentation puts the legal basis on you, not on the tooling. Nothing in this flow establishes consent. It can only refuse to carry a row that does not have it.
- Triggers
- Two
- Runs
- Weekly, plus on write
- Stack
- 4 tools
- Durable execution
- Not required
The flow
The top track keeps one row per person as records arrive. The bottom track wakes weekly and decides, per row, whether it is allowed to leave — and only what is allowed is ever hashed and sent.
A customer record arrives or changes
Sheets · Airtable
Normalise before comparing
n8n · Make
One row per person
Sheets · Airtable
Every week
n8n schedule
Take the rows still in scope
n8n · Make
Consent on file for this use?
n8n IF
Hash, then upload
Meta · Google Ads
Remove them, then say why
Google Ads · Meta
Step by step
- 01
A customer record arrives or changes
Sheets · AirtableAn order, an enquiry, a form. Record the purpose it was collected for and the date, in the same write. Reconstructing that months later is guesswork, and guesswork is not a legal basis.
- 02
Normalise before comparing
n8n · MakeLowercase, trim, phones to a single international format, and for Gmail addresses strip the periods and any plus-suffix. Duplicates are mostly formatting, not different people — two rows that look different collapse into one only after this step.
- 03
One row per person
Sheets · AirtableMerge onto the normalised identifier, keeping the earliest consent record and the most recent contact detail. Carry the purpose and the date on the row itself, so the next step can read them rather than assume them.
- 04
Every week
n8n scheduleWeekly is deliberate. Platform audiences drift out of step with the source list in both directions — people are added, and people withdraw consent — and each direction needs its own operation, because uploading is additive and does not remove anyone. A list refreshed once at launch is a list that is wrong by the second month.
- 05
Take the rows still in scope
n8n · MakeDrop rows past your own retention period, and rows whose recorded purpose does not cover advertising. This is the step that shrinks the list, and it is meant to.
- 06
Consent on file for this use?
n8n IFRead from the row, never inferred. A purchase is not consent to marketing, and an absent value is a no. The operator decides what counts as consent and how it was obtained; the workflow only enforces the decision that was already recorded.
- 07
Hash, then upload
Meta · Google AdsSHA-256 after normalisation, so the platform receives a hash and never the address itself. Normalising and hashing in the wrong order produces a different digest, which reads as a low match rate rather than as an error.
- 08
Remove them, then say why
Google Ads · MetaWithholding a row from the next upload does not take the person out of the audience already sitting on the platform — they stay targetable until membership expires. Removal is its own job, and on Google Ads a remove operation cannot share a job with create operations. Then the count and the reason go to a named owner: expired retention, purpose mismatch, consent withdrawn. A row silently dropped looks identical to a row that was never there.
What the platform and the law will not allow
The first three are the platforms' own rules on how the data may be sent and how large the list has to be. The last two are Hong Kong's, and they sit upstream of every platform rule — a list that satisfies Meta and Google can still be unlawful to have built.
Uploading is additive, so consent withdrawal needs its own operation. On Google Ads, remove operations cannot be mixed with create operations in the same job, and a remove_all must be the first operation in its job or the run fails. Until that job runs, a person who withdrew consent stays a targetable member of the audience.
Google Ads API — Customer Match: get startedMeta requires the upload to be hashed and states the algorithm exactly: "To create audiences, you must share your data in a hashed format to maintain privacy", and "You must hash data as SHA256; we don't support other hashing mechanisms." Meta also puts ownership plainly — "As the owner of your business's data, you are responsible for creating and managing this data."
Meta Marketing API — Customer list custom audiencesGoogle Ads requires the same: "email addresses, first names, last names, and phone numbers must be hashed using the SHA-256 algorithm before being uploaded", and for Gmail addresses the username must have periods and any plus-suffix removed first. That normalisation step is also what makes deduplication work, which is why it belongs upstream rather than at the upload.
Google Ads API — Customer Match: get startedCustomer Match sets both a floor and a ceiling on the list itself: "Upload at least 5,000 members to increase the chance of having enough matched, active users for targeting", and the membership_life_span "must be no more than 540" days. A small clean list can therefore be too small to target with, which is a real outcome of doing the hygiene properly.
Google Ads API — Customer Match: get startedGoogle states the sourcing rule as policy, not guidance: "Per policy, you can only upload data that you acquired yourself (first-party). You cannot buy email lists from third parties and upload them." The consent field must be populated on upload, and data from unconsented EEA users will not be processed for ad personalisation.
Google Ads API — Customer Match: get startedUnder Hong Kong's PDPO, DPP3 "prohibits the use of personal data for any new purpose which is not or is unrelated to the original purpose when collecting the data, unless with the data subject's express and voluntary consent", and DPP2 requires that personal data "is not kept longer than is necessary for the fulfilment of the purpose for which the data is used". An address collected to fulfil an order is not, without more, an address you may advertise to.
PCPD — The Personal Data (Privacy) Ordinance at a glanceFor direct marketing specifically, Part 6A of the PDPO requires informed consent before using personal data for direct marketing or transferring it to a third party for that purpose. Consent requires "an explicit indication by the data subject", and "silence cannot constitute consent" — so an unticked box, or a row with nothing recorded against it, is a no.
PCPD — The Personal Data (Privacy) Ordinance at a glance
When this is not worth building
Three cases where the upkeep costs more than the duplicates, and one of them is a reason to stop entirely.
You cannot say, for each row, what it was collected for. That is not a scheduling problem to be solved with a weekly job — it is a reason not to upload the list at all until the record exists, because the workflow can only enforce a consent decision that somebody already made and wrote down.
The list is small enough that one person recognises the duplicates on sight. They will merge them faster than a weekly job will, and the job becomes another place personal data is copied to for no gain.
The platform audiences are not actually used for targeting. Keeping a customer list in step with an audience nobody spends against means you are holding personal data on an ad platform to no end, which is exposure without a corresponding benefit.