Offline Conversions from Google Sheets to Meta: Columns, Hashing and Deduplication
How to send sales and qualified leads from a Google Sheet to Meta via the Conversions API: required columns, date and phone formats, hashing and the 7-day limit.

On this page
Plenty of businesses run their pipeline in a Google Sheet: a sales rep adds a row, marks it "qualified", later "won". Meta never hears about it, because the Pixel sees your website, not your spreadsheet. The Conversions API lets you pass those changes to Meta as offline conversions, meaning events that happened away from your site. This guide shows how to lay out the columns, how the data is hashed, how double counting is avoided, and where this approach stops working.
What are offline conversions in Meta?
They're events your business knows about and the browser doesn't: a phone sale, a signed contract, a lead qualified after a call. Meta now takes them through the same Conversions API as website events. It describes its older, separate offline conversions interface as legacy and recommends the Conversions API for new integrations.
An event from a sheet uses action_source = system_generated, i.e. raised by a business system. The key rules from Meta's server event parameters:
event_timeis when the event really happened, at most 7 days before sending- one event that's too old makes Meta reject the whole request
- each event needs at least one customer identifier
Which columns does the sheet need?
Column names are up to you, because you map them in the panel. What matters is the content.
| Column (example) | What it holds | Watch out for |
|---|---|---|
id | A stable record ID, e.g. your CRM number | Not the row number, which changes when you sort. No emails or names |
status | Business status, e.g. QUALIFIED, CONVERTED | Case must match the flow |
status_changed_at | When the record entered that status | A date cell or ISO 8601 with time zone. Not the sync time |
email | Customer email | Hashed before sending |
phone | Phone with country code, e.g. +14155550123 | Text column, so the sheet keeps the plus sign |
meta_lead_id | Lead ID from a Meta Instant Form | Text only, or the sheet may round the long number |
value, currency | Amount and ISO 4217 code, e.g. EUR | Required for Purchase |
consent | TRUE when you have a lawful basis to share the data | Don't fill it in wholesale without checking your process |
The minimum is record ID, status, time, consent and at least one person identifier: email, phone or Meta lead ID.
Why does the status change time matter so much?
Because Meta ties a conversion to the moment it happened. If a tool stamped events with the sync time, a Friday sale would look like a Monday sale. So Convs never substitutes the read time for a missing date: a row without one is rejected, and the message points to the first failing row number without showing any customer data.
Date cells are read in the sheet's own time zone. Text dates must include a zone, e.g. 2026-04-14T10:30:00-05:00.
How is data from the sheet hashed?
Meta requires email and phone to be normalised and SHA-256 hashed, while the lead ID is sent as is. Following Meta's customer information parameters:
- Email: trim spaces, lowercase, then SHA-256.
Jane.Doe@Example.comandjane.doe@example.comproduce the same hash. - Phone: strip spaces, dashes and brackets; the country code is mandatory; hash the digits only.
+1 (415) 555-0123becomes the hash of14155550123. - Meta lead ID: not hashed, because Meta knows it in that form.
Convs does this on the server. A phone number without a country code is rejected rather than guessed, because a wrong prefix produces a hash that matches nobody. Hashes are still personal data under laws like the GDPR; see GDPR and hashing in the Conversions API (not legal advice).
How do you map statuses to Meta events?
A flow defines which source status becomes which Meta event. For example:
| Status in the sheet | Meta event |
|---|---|
QUALIFIED | QualifiedLead |
CONVERTED | Purchase (with value and currency) |
LOST | not mapped, so nothing is sent |
Pick event names that match what you'll later use in a campaign or custom conversion. One source can feed several destinations (two Pixels, say), and each destination has its own delivery status. For designing stages Meta can learn from, read CRM lead stages for Meta.

How does deduplication work with a sheet?
A sheet read every minute means the same row is seen hundreds of times. If every read sent an event, Meta would get a flood of duplicates. There are two layers of protection:
- Business event: a record ID plus status from a source is a one-off conversion. Re-entering the same status doesn't create a new one.
- Delivery: each event has a stable
event_idderived from the record ID and status, and the queue won't send the same event to the same Meta dataset in the same mode twice, even from two different connections.
Retries keep the same event_id, so if Meta's response is lost in transit, Meta can still deduplicate the repeat. The Pixel never sends sheet events, so there's nothing to reconcile with the browser. More in Pixel and CAPI event deduplication.
What can't a sync reconstruct?
A sheet shows a row's current state, not its history. If a rep moves a row from QUALIFIED to CONVERTED within a minute, a read might only see the second status. If statuses can change faster than the sheet is read, log events in a separate tab: one row per change, each with its own ID.
How do you check that events arrive?
- Create a Meta destination in test mode with the code from Events Manager.
- Change the status of one row containing test data.
- Check the delivery in the panel: "Accepted by Meta" means a response with
events_received: 1. - In Events Manager's test events tab, confirm the event arrived with the right name and parameters.
- Only then create a production destination and flow. Test and production keep separate deduplication.

Acceptance says nothing about how well Meta matched the event to a person. Events Manager shows that as Event Match Quality; see how to improve Event Match Quality.
Common errors and their causes
| Symptom | Cause | Fix |
|---|---|---|
| Row rejected: missing time | Empty date column, or text without a time zone | Fill in the date or add the zone in ISO 8601 |
| Row rejected: phone | Number without country code | Store +<country code>… in a text column |
| Event too old | Status changed more than 7 days ago | Meta won't take it. Keep the sheet current |
| Nothing is sent | No active flow, or the status uses different letter case | Check the flow mapping |
| Data change rejected | Editing a row already reported with the same status | A new business event needs a new record ID |
When is a spreadsheet the wrong source?
- You update statuses once a week. Some events will pass the 7-day limit and be lost. The sheet has to be kept current.
- Your leads come from Meta Instant Forms and you want conversion leads optimisation. Meta expects CRM events in a specific format for that goal. It's simpler to connect the form directly and update stages on the person's record. The requirements are in Meta conversion leads optimisation.
- You have tens of thousands of active rows. At 500 rows per minute a full pass takes a while; a server-side integration suits that volume better.
- The sheet has no person identifier at all. Without email, phone or lead ID, Meta has nothing to match against.
Next step
Add the columns from the table, connect the sheet in test mode and change one row's status. Read more about delivery on the Meta Conversions API feature page; plans are on the pricing page.
Frequently asked questions
Do I need to install Apps Script in my sheet?
No. You connect the sheet by signing in with Google and picking the file in Google's own file picker. The app only gets access to the file you choose, not your whole Drive, and it only reads data.
How quickly does a change in the sheet reach Meta?
The server reads the sheet every minute, up to 500 rows per pass, so a full pass over a large sheet takes a few minutes. A new status goes into the delivery queue and from there to Meta, usually within minutes of the edit.
What happens if I change the status of a row that was already sent?
A new status is a new event, so it is sent if your flow maps it. The same record ID with the same status is a one-off conversion: re-entering a status doesn't create a second conversion, and changing the data of a pair that was already reported is rejected.
Why does the Meta lead ID change in my sheet?
Lead IDs are long numbers, and a spreadsheet may treat them as numeric and round the last digits. Format the column as plain text, or paste IDs with a leading apostrophe, before you start syncing.
Are sheet events enough for conversion leads optimisation?
Meta's conversion leads goal expects CRM events in a specific format, tied to leads from its native Instant Forms. If your leads come from Instant Forms, connect the form directly and update stages on the person's record instead. A sheet works well for sales and for leads that don't come from Meta forms.

