Odoo server-side tracking means your Odoo backend — not the visitor’s browser — reports completed purchases directly to Google and Meta. Odoo captures the ad click identifier when a visitor lands, stores it against the sales order at checkout, and transmits the conversion via API when the order reaches a confirmed or paid state. Because the transmission happens server-to-server, it survives ad blockers, closed tabs, and cookie expiry, and it only reports sales that actually completed.
That’s the summary. The rest of this article is how you actually build it, and the decisions that determine whether it works properly or just appears to.
If you’re not yet sure whether this is your problem, start with why Odoo conversion tracking fails — this piece assumes you’ve already diagnosed it.
Odoo server-side tracking architecture: two tracks, different jobs
The most common mistake is treating this as a choice between browser tracking and server-side tracking. It isn’t. You need both, doing different jobs.
The browser track handles engagement. Google Tag Manager and the Meta Pixel capture high-volume, non-financial events — product views, searches, add-to-cart. These are behavioural signals. They benefit from firing immediately, they don’t need to be perfectly complete, and if an ad blocker eats some of them, your reporting is slightly less rich but nothing breaks.
The server track handles money. When an order is confirmed and paid in Odoo, the Odoo backend calls the ad platform APIs directly. This bypasses the browser entirely. Every paid order gets reported regardless of ad blockers, browser crashes, page refreshes, or the customer closing the tab three seconds after checkout.
The division is not arbitrary. It maps to a real property of the data: engagement events are cheap to lose and expensive to verify; transaction events are expensive to lose and cheap to verify. Your database already knows, with certainty, which orders are real. The browser never does.
This split also gives you a meaningful cost advantage. Running a full server-side Google Tag Manager container means provisioning and paying for tagging server infrastructure. Reporting transactions directly from Odoo — which is already running — achieves most of the accuracy benefit without that overhead. You are using a server you already have.
What runs where
| Signal | Track | Why |
|---|---|---|
| Product view, search, view cart | Browser | High volume, low value per event, needs to be immediate |
| Add to cart, begin checkout | Browser | Behavioural intent signal; incompleteness is tolerable |
| Purchase / conversion | Server | Must be complete, must be verified against payment, must survive blocking |
| Refund / cancellation | Server | Only the database knows this happened |
| Manually created order | Server | Never touches a browser at all |
Step 1: Capture the click identifier and make it survive
When someone clicks a Google or Meta ad, the ad platform appends an identifier to the landing URL — gclid for Google, fbclid for Meta. That identifier is what ties an eventual sale back to the specific ad click. If you lose it, you lose the attribution.
The naive approach stores it in browser local storage. That’s the approach that fails, and Safari’s cookie restrictions are the reason — client-side storage is exactly what privacy tooling targets.
Instead, intercept the parameter server-side on page load and write it into the Odoo session. A controller in the website module reads gclid or fbclid from the inbound URL and persists it in the server-side session rather than handing it to the browser to look after. This matters because the Python backend needs that value at order confirmation, potentially days later, and because a value held server-side isn’t subject to the browser storage rules that purge JavaScript-set cookies.
Then, at checkout, move it from the session onto the sales order record. Add technical fields on the sale.order model — x_gclid, x_fbclid — populated automatically at order creation. This is the critical transition: it moves the attribution from a temporary session, which expires, to a permanent database record, which doesn’t.
Once the identifier is on the order, the attribution is immutable. A purchase that happens six weeks after the click still knows which click it came from, because the association was written to the database at checkout and nothing in the browser can erase it.
Alongside the click identifier, persist two more things in the session: the hashed customer identity data (see step 3) and the visitor’s consent choice (see step 5). All three need to be available to the backend at the moment the order is confirmed.
Step 2: Trigger on state change, not on a timer
This is a design decision where the simpler option is defensible but costs you something real, and it’s worth making deliberately rather than by default.
The transmission should fire when the sales order changes state to Sale or Done, following a successful payment. Not on a schedule. Not in a nightly batch.
The alternative is a scheduled action that sweeps for unsent events and transmits them periodically. This is a common pattern — one widely-used Odoo Meta CAPI module ships with a scheduled action defaulting to every 30 minutes, per its own documentation. Batching is simpler to build, easier to rate-limit, and it does eventually deliver the data. But it costs you in three ways:
- Attribution latency. Ad platform optimization algorithms use recency. A conversion reported eight hours late is worth less as an optimization signal than one reported in seconds.
- Debugging difficulty. When a conversion goes missing, a batch window makes it much harder to reconstruct what happened and when.
- Silent accumulation. If the scheduled job fails, it can fail repeatedly and quietly, and you find out days later from a reporting discrepancy.
State-change triggering means the API call is a direct consequence of a database event you can point to. The order moved to Sale; the conversion fired. If it didn’t fire, you know exactly which order to look at.
It’s a useful question to put to any vendor or developer — if the answer is “it runs on a schedule,” you now know what you’re trading away and can decide whether it matters for your volume.
Step 3: Identity matching, and the hashing detail that breaks implementations
Click identifiers get lost. Someone clicks an ad on their phone, thinks about it, and buys on their laptop two days later. The gclid is gone. First-party data matching is the fallback that recovers those conversions.
Both platforms accept hashed customer data — email, phone, name — and match it against their own user records without either side exposing the raw values. Google calls this Enhanced Conversions; Meta calls it Advanced Matching. The mechanism is the same: normalize the data, hash it with SHA-256, send the hash.
The normalization step is where implementations quietly break, because a hash mismatch is indistinguishable from a non-match. You get no error. You just get worse matching, and no signal telling you why.
Per Google’s Google Ads API documentation and Enhanced Conversions setup guidance, the required normalization is:
- Lowercase everything and strip leading/trailing whitespace
- Phone numbers must be formatted to E.164 (
+61412345678) - Gmail and Googlemail addresses specifically must have periods removed from the username portion and any
+suffixstripped before hashing - The output must be hex-encoded SHA-256
- Normalization applies to address fields too — street, city, region, postal code, country — not just email, phone, and name
That third point is the one that catches people. [email protected] and [email protected] are the same Google account, and Google expects the normalized form. Hash the raw string and the match silently fails for a meaningful share of your customers, because Gmail addresses are a large share of most consumer email lists.
One precision worth stating clearly, because it’s commonly muddled: the GCLID itself is never hashed. It’s a plaintext click identifier and it’s matched as-is. Hashing applies only to customer-provided identity data. Google’s API documentation is explicit that GCLID-only uploads shouldn’t set user identifier fields at all.
Google also permits sending raw customer data and letting Google’s own tag perform normalization and hashing. For a server-side implementation transmitting via API, you’re doing it yourself, so the rules above are yours to get right.
Step 4: Deduplication — stop counting every sale twice
You now have two tracks that can both report a purchase. Without deduplication, Meta counts the sale twice and your reported ROAS becomes fiction.
The fix is a shared event identifier. Use the Odoo sales order reference — SO0933 and so on — as the event ID. It’s already unique, already meaningful, already in your database, and it makes debugging trivial: when a conversion looks wrong, you can search for the order reference in both Odoo and the platform’s event log and see both sides of the same transaction.
Both the browser pixel and the server-side API payload send that same identifier. Meta’s Conversions API documentation specifies that events are deduplicated when they carry a matching event ID and the same event_name — both conditions, not either. Meta’s browser SDK expects the value as eventID; the server payload field is event_id. Same value, different casing convention on each side, which is an easy thing to get subtly wrong.
One thing worth correcting, because it’s widely assumed the other way round: the server-side event does not automatically override the browser one. Per Meta’s documentation, within a 48-hour window Meta retains whichever matched event arrived first, regardless of source — and where browser and server events land within roughly five minutes of each other, Meta gives preference to the browser event’s data.
So the server track isn’t functioning as an authoritative record that overwrites the browser. It’s a reliability backstop. Its value is that it captures the conversions the pixel never reported at all — the ad-blocked, the closed-tab, the cookie-expired — while the pixel continues to win the races it manages to finish. That’s a less satisfying mental model than “the server is the source of truth,” but it’s the one that matches how the platform actually behaves, and it explains why you run both tracks rather than switching the pixel off.
Step 5: Enforce consent in the database, not the browser
Most Odoo tracking content treats consent as a settings checkbox. It’s an architectural decision, and getting it right is what makes the whole system defensible.
The principle: check consent server-side, before the API call is constructed. If a visitor has denied marketing consent, the backend strips the marketing identifiers and personal data from the payload before anything is transmitted.
Why this placement matters: browser-layer consent enforcement can be circumvented with developer tools or script injection. A check that executes in your database, before an outbound request exists, cannot be. If you are ever asked to demonstrate that consent was actually enforced rather than merely displayed, the server-side version is the one you can evidence.
In practice this means persisting the visitor’s consent choice alongside the click identifiers — a consent level field on the sales order — and gating the transmission logic on it.
Handling denial without losing everything
Denied consent doesn’t have to mean zero signal, and this is where the platform mechanics get genuinely interesting.
Google Consent Mode v2 operates in two modes, and the difference matters (Google, About consent mode):
- In basic consent mode, Google tags are gated entirely until the visitor makes a consent decision. Deny, and nothing reaches Google at all.
- In advanced consent mode, tags load regardless, and on denial they send an anonymous cookieless ping — a signal that something happened, carrying no identifiers.
Those cookieless pings feed Google’s conversion modeling. Per Google’s consent mode modeling documentation, aggregated pings train advertiser-specific models rather than leaving Google to fall back on a generic one. Google states these pings are aggregated and are not used to track individuals, build remarketing lists, or create user profiles.
Consent Mode v2 governs this through four parameters, which split into two functions worth understanding separately (Google Tag Platform documentation):
| Parameter | Function |
|---|---|
ad_storage |
Whether ad-related cookies/identifiers may be stored — upstream |
analytics_storage |
Whether analytics cookies may be stored — upstream |
ad_user_data |
Whether Google may use the data for advertising — downstream |
ad_personalization |
Whether Google may use it for personalized advertising — downstream |
Meta’s equivalent is Limited Data Use (LDU), flagged on the Conversions API event via data_processing_options, with data_processing_options_country and data_processing_options_state controlling the geographic scope (Meta, Data Processing Options).
The trade-off under LDU is specific and worth stating precisely, because it’s often described too narrowly: the conversion still counts toward measurement, attribution reporting, and aggregated modeling, but Meta is restricted from using that person’s data for ad personalization — which includes building or expanding Custom Audiences, Lookalike audiences, and retargeting pools, among other uses. It’s a “measure but don’t personalize” flag, not a “don’t count” flag.
LDU applies to users in US states covered by Meta’s State-Specific Terms. That list has expanded over time as more state privacy laws have taken effect, so check Meta’s current documentation rather than relying on any list published in an article — including this one.
So a well-built consent architecture doesn’t choose between compliance and measurement. It degrades gracefully: full data with consent, modelled data without.
Step 6: Plan for failure, because it will be silent
In the implementations we’ve been called in to review, this is the part most often missing — and the consequences are quiet and expensive.
An API call to Google or Meta can fail for entirely mundane reasons — a rate limit, a token expiry, a transient network fault, a temporary platform outage. When it does, that conversion is gone. It doesn’t retry itself. Nothing alerts anyone. The order sits in Odoo looking perfectly normal, and your ad platform simply never learns the sale happened.
Multiply that across weeks and you get a systematically under-reported campaign that you then under-fund, because the data says it isn’t working.
A production implementation needs three things:
1. Log every API response against the order. A conversion tracking tab on the sales order recording success, failure, and the actual API response body. When something looks wrong three weeks later, you need to be able to open the order and see exactly what the platform said at the time.
2. Retry failures automatically, with a bounded limit. A scheduled job — twice daily is a reasonable cadence — sweeps for failed transmissions and retries them. Bounded, because infinite retries against a permanently malformed payload just generate noise. Five attempts is a sensible ceiling.
3. Alert a human when retries are exhausted. Once an order hits the retry limit, email an administrator. This is the step that converts a silent failure into a visible one, and it’s the difference between finding out in an hour and finding out in a quarter.
Note that logs alone are not sufficient. A log nobody reads is a silent failure with extra steps. The alert is the part that matters.
On sensitive data in those logs: event logs often contain IP addresses and user agents. Set a retention period and purge on schedule. Thirty days is a reasonable starting point — it’s long enough to debug a reporting discrepancy and it’s the default in at least one established Odoo tracking module, which suggests it’s a workable balance in practice. Keeping detailed visitor logs indefinitely creates a data protection liability that outlives any debugging value.
Step 7: Track the orders that never touch a browser
Here’s a scenario no published Odoo tracking guide addresses, and it’s common in real businesses.
A customer sees your ad. They call instead of checking out online, or they email, or they walk into your trade counter. Your team creates the sales order manually in Odoo. The sale is real, the ad caused it, and every browser-based tracking approach is structurally blind to it — there was no web session, so there is nothing to track.
For businesses with a meaningful phone or offline order component, this can be a substantial share of ad-driven revenue that never appears in your conversion data. Your campaigns look worse than they are, permanently.
The solution is a manual conversion path. Add a “Track Conversion” flag to the sales order. When staff create an order they know came from an ad, they check it, and optionally attribute a source. On confirmation, the purchase event transmits like any other.
The honest limitation: without a click identifier, you cannot attribute the sale to a specific campaign or keyword. Matching falls back entirely to hashed customer data, and the platform decides what it can associate. You’re recovering the conversion, not the full attribution path — which is still considerably better than the sale being invisible.
Important if you’re building this now: the Google API path has changed
Google is moving offline conversion ingestion to its new Data Manager API. Per Google’s own developer announcement (May 2026), as of 15 June 2026 the legacy Google Ads API offline conversion upload path — UploadClickConversions — stopped accepting new adopters. Developer tokens that hadn’t already used offline conversion imports in the qualifying window are rejected by an allowlist check. Tokens already on the allowlist can continue on the legacy path for now.
The practical consequence: if you are starting a new Odoo offline conversion implementation today, you should be building against the Data Manager API, not the legacy endpoint — the legacy route may simply reject your token. If you have an existing implementation on the old path, it should continue working, but plan a migration.
There has been secondary reporting of a full sunset of the legacy path in 2027. We haven’t found that date confirmed on a Google-owned page, so treat it as directional rather than firm, and check Google’s current documentation before making a timeline commitment.
Putting it together
The full pipeline, end to end:
- Visitor arrives from an ad → server-side controller captures
gclid/fbclidinto the Odoo session - Visitor browses → browser track (GTM, Meta Pixel) reports engagement events via a data layer fed from Odoo
- Visitor checks out → identifiers move from session to permanent fields on the sales order
- Payment succeeds, order moves to Sale/Done → state change triggers the API call
- Backend checks consent → strips identifiers and PII if denied, or applies LDU / cookieless ping handling
- Backend normalizes and SHA-256 hashes customer data, attaches the order reference as event ID
- Backend POSTs to Meta CAPI and the Google Ads API
- Response logged against the order; failures queued for retry; exhausted retries alert an administrator
Every step in that chain exists because a specific failure mode would otherwise occur. Skip step 1 and you lose long-window attribution. Skip step 4 and you accept latency. Skip step 5 and you have a compliance exposure. Skip step 6 and your match rates quietly degrade. Skip step 8 and you never find out about any of it.
Do you need a module or a custom implementation?
An honest answer, given we sell one of these.
A commercial module is likely sufficient if: you run a standard web checkout, batched transmission every 30 minutes is acceptable, you don’t need manual or offline order tracking, and your consent requirements are straightforward. Several Odoo tracking modules are genuinely well-built and better documented than most commercial software. Buying one is often the right call, and module pricing sits in the low hundreds of dollars per Odoo version — an order of magnitude below any custom build.
A custom implementation earns its cost if: you need immediate state-change triggering rather than batched sending, you have meaningful manual or phone order volume, you need automated retry with alerting, you have non-standard consent requirements, or you’re running multiple websites or warehouses with different tracking logic.
One factor specific to modules that’s worth weighing before you commit: Odoo modules are licensed and released per Odoo version. Each major upgrade means the module needs a version released for it, and you’re dependent on the vendor’s release schedule — which, if you upgrade Odoo aggressively, can put your conversion tracking on someone else’s timeline. Modules that extend other paid modules compound this, since you’re tracking compatibility across a chain. This isn’t an argument against buying; established vendors keep pace and the price difference is substantial. But if your Odoo upgrade cadence is tightly controlled or contractually driven, it belongs in the decision.
The deciding question is usually not technical sophistication. It’s whether the gap between “eventually correct” and “correct now, verifiably” is worth money in your business. At low ad spend it generally isn’t — the measurement error costs less than the fix. As spend scales, misallocated budget from bad data starts to exceed the cost of correcting the data. Where exactly that crossover sits depends on your margins and your spend, and it’s worth actually working out rather than guessing.
Talk to us about your Odoo tracking setup — we’ll tell you honestly which of the two you need.
Frequently asked questions
What is Odoo server-side tracking?
Odoo server-side tracking is an architecture where your Odoo backend reports completed purchases directly to Google and Meta via their APIs, rather than relying on browser scripts. Odoo captures the ad click identifier when a visitor lands, stores it on the sales order, and transmits the conversion when the order is confirmed and paid. Because it’s server-to-server, it isn’t affected by ad blockers, cookie expiry, or the customer closing their browser.
How do I send purchase events from Odoo to the Meta Conversions API?
Store the fbclid from the landing URL in the Odoo session, transfer it to a field on the sales order at checkout, then trigger a server action on the order reaching Sale or Done state that POSTs the purchase payload to Meta’s CAPI endpoint. Include the Odoo order reference as the event ID so Meta can deduplicate against the browser pixel, and hash all customer data with SHA-256 before transmission.
How do I deduplicate Meta Pixel and Conversions API events in Odoo?
Send the same unique identifier on both signals — the Odoo sales order reference works well. Meta deduplicates when both the event ID and the event_name match. The browser SDK expects it as eventID; the server payload uses event_id.
Where should I store the GCLID in Odoo?
In the Odoo server-side session on capture, then on a dedicated field on the sale.order record at checkout. Avoid browser local storage — it’s subject to the privacy restrictions that cause attribution loss in the first place, and the Python backend needs the value at order confirmation regardless.
Does server-side tracking bypass cookie consent requirements?
No, and any implementation that treats it that way is a liability. Server-side tracking changes where consent is enforced, not whether it applies. Done properly it’s more defensible than browser-side enforcement, because a database-level check can’t be bypassed with developer tools. Under denied consent you should still be stripping identifiers and using Google’s cookieless pings or Meta’s Limited Data Use flag.
Can I track conversions for orders created manually in Odoo?
Yes, with a caveat. Add a flag on the sales order that triggers transmission on confirmation. Because there’s no click identifier, attribution falls back to hashed customer data matching — you recover the conversion but not the specific campaign attribution path.




