CRM
Most attribution tools treat a CRM stage as revenue. Odoo gives Attribi something better: a confirmed sales order, with a real total and a real currency, which is the same number your finance team reports. Attribi reads those orders on a schedule and uploads each one to every ad platform you have connected as a closed-won conversion. This connection runs one way only, so nothing is written back into Odoo and no Attribi fields appear on your records.
No credit card required · 5-minute setup
Attribi reads sales orders Odoo itself has moved to confirmed or done, using the order state rather than a label anyone can rename, and uploads the order total in the currency the order carries. Quotations are not revenue and are never counted.
Name the crm.lead stages that mean qualified for your team and each lead reaching one fires a qualified conversion. Odoo installs rename stages freely, so Attribi never assumes a default: leave the list unset and the qualified pass does not run at all.
Odoo carries an expected revenue figure on every CRM lead. Attribi does not read it, and a qualified conversion is sent with no value attached rather than a number nobody has been paid. Only a confirmed order puts money on a conversion.
Attribi connects over Odoo’s own API with a user API key rather than an OAuth app, so a self-hosted or Odoo.sh instance connects exactly like a hosted one. Nothing is installed into Odoo and no module has to be approved.
Planned, not live today. Listed so you can see what this integration does not do yet, before you sign up.
Writing the campaign, source and journey onto the Odoo record so your sales team sees them where they work, and pushing a new lead into Odoo when someone converts on your site. Neither exists today: Odoo is read-only, unlike the HubSpot, Salesforce, Zoho and Pipedrive connections.
Not yet available.
Odoo Online only allows external API access on its Custom plan. One App Free and Standard both block it, and no credentials will work on either. That is by far the most common reason a connection attempt fails, so rule it out before anything else. Odoo Online Custom, Odoo.sh, or a self-hosted instance all work.
In Odoo, open your user record under Settings, Users and Companies, and create an API key from the Account Security tab. Odoo shows it once. Paste it into Attribi along with your Odoo URL, database name, and the login email of that user. Attribi validates the credentials live before saving.
The user needs read access to Sales Orders, CRM Leads, Contacts, and CRM Stages. Contacts is the one people miss: without it Attribi connects successfully and then matches nobody, because the buyer’s email lives on the linked contact. Then list the CRM stages that mean qualified.
Step-by-step walkthrough: Odoo Revenue Attribution: Confirmed Orders Are Not Collected Cash
Odoo marketing attribution in Attribi reads two models. A sale.order in state sale becomes a closed-won conversion carrying amount_total and the order's own currency. A crm.lead entering a stage you name becomes a qualified conversion with no value. Nothing is written back. The connection is JSON-RPC with a user API key, which Odoo Online only allows on the Custom plan.
Odoo marketing attribution is the matching of a confirmed Odoo sales order, or a CRM lead that reached a named stage, back to the ad click that started that customer's journey, and the upload of that outcome to the ad platform as an offline conversion. In Attribi it is read-only. This table is the entire read surface.
| Odoo model | Filter Attribi applies | Fields read | What it becomes |
|---|---|---|---|
sale.order | state in (sale, done), write_date after the last pull | id, name, amount_total, currency_id, partner_id, write_date, date_order | Closed-won, value = amount_total |
res.partner | partner_id values from one page of orders, one batched read | id, email | The email the order is matched on |
crm.lead | stage_id.name in your qualified list, write_date after the last pull | id, name, email_from, stage_id, write_date | Qualified, no value |
crm.stage | None, a listing ordered by sequence | name | The stage picker at connect time |
The won pass runs first, paging sale.order 100 at a time in write_date order with one res.partner read per page. The qualified pass runs second, and only if you have named at least one stage; an empty list means it does not run rather than guessing. Both share one watermark, last_revenue_sync_at, which moves only when both passes finished their window. A first pull reaches back 30 days.
On the state filter, verified 2026-10-12 in Odoo's source: Odoo 16 had a fifth state, done, shown as Locked; Odoo 17 removed it and added a locked boolean, which 18 and 19 keep. The filter names both values, so a locked order is counted on every version. A quotation, sent or not, is never read.
A store on Odoo eCommerce, or any team that confirms quotations into orders, records every sale as a sale.order in state sale. For them this is the strongest revenue source Attribi has: the order total your accountant sees, tax and delivery included, in the order's own currency.
A lead-generation team that lives in the CRM app and never raises a quotation gets the opposite. Their opportunities are crm.lead records, and moving one to a Won stage sends nothing, because the won signal is the order state, not the opportunity stage. They get the qualified signal from the stages they name, with no value, and no revenue at all. Odoo carries an expected_revenue figure on every opportunity; Attribi does not read it, because a forecast uploaded as a conversion value trains bidding on money that may never arrive.
JSON-RPC. Every call is a POST to /jsonrpc on your instance. Connecting calls common.authenticate(db, login, api_key, {}), which returns a numeric user id on success and the boolean false on bad credentials. The API key stands in for the password, as Odoo's External API reference documents it (verified 2026-10-12): generate one under Preferences, Account Security, New API Key, and Odoo shows it once. Odoo answers failures with HTTP 200 and an error body; Attribi reads the body, so the connect form shows Odoo's own message.
Odoo 19 changes the picture. Its External JSON-2 API (verified 2026-10-12) lives at /json/2/<model>/<method>, takes the API key as a bearer token, and marks /xmlrpc, /xmlrpc/2 and /jsonrpc deprecated, with removal scheduled for Odoo 22 in autumn 2028. Attribi still speaks /jsonrpc. That works on 19 through 21; moving to JSON-2 is on our side. Also on 19, a new key is created with a duration, and when it expires the daily pull fails with an authentication error until you paste a fresh key.
On Odoo Online, external API access is only available on the Custom plan. Odoo's own reference says it is not available on One App Free or Standard, and the pricing page lists External API under Custom alone (both verified 2026-10-12). Standard is paid, has every app, and still cannot be connected. Most setup pages, ours included until today, say a paid plan is enough. Odoo.sh and Enterprise on-premise come with the Custom licence, so the API is included. Self-hosted Community has no plan gate but still needs https:// and a publicly resolvable hostname.
Whichever host, connecting runs two live checks before saving: it authenticates, then runs one res.partner search limited to a single record. The second catches a user who can authenticate but cannot read Contacts, who would otherwise match nobody. The user needs read access to Sales Orders, CRM Leads, Contacts and CRM Stages.
Since 5 October 2026 it is withheld. Before that it was uploaded in US dollars.
The pull reads the currency off the currency_id pair, which Odoo returns as [id, name] with the ISO code as the name, so [1, "EUR"] is EUR. The old line ended || 'USD', so an order with an empty or unrecognised code reached every ad platform as dollars. PR #537, commit 4e7a7c2, replaced that with a hold. An absent code falls back to a manually set default currency; a present but non-ISO code never does; and when nothing resolves, the upload is not sent, the order is mirrored to the deal ledger with a null currency, and the watermark is held so it stays in the next pull's window.
The ISO check matters on Odoo because res.currency.name is a three-character field and you can define your own currencies: a loyalty-points currency PTS has the right shape and would once have uploaded as money. The hold is bounded: three further daily pulls retry, then the order is skipped with a message saying so. Refunds and zero-value orders need no currency and are never held.
One premise deserves correcting. The commit assumed currency_id is required on sale.order; in Odoo 18's source it is computed from the pricelist's currency, falling back to the company currency, so every order carries one through a default rather than a constraint.
Yes, and Attribi does not reconcile it. If a connector mirrors paid Shopify orders into Odoo as sales orders, the same sale reaches Attribi twice: from the Shopify webhook within minutes of payment, and from the Odoo pull the next morning.
If the Shopify order email and the Odoo contact email agree, the Shopify event marks the lead won first and the Odoo pass does not upload again, but the order still lands in the deal ledger as a second row, so anything summing orders rather than leads counts it twice. If the emails differ, two leads are created and two closed-won conversions uploaded. Google Ads and Meta dedupe on the lead plus the stage, so two leads are two conversions to them too.
An earlier cross-source heuristic was removed because it suppressed real sales. The real fix is a shared reference, the Shopify order number threaded into Odoo's client_order_ref or origin field, with a pairing you configure. It is not built. Until it is, connect one source per sale.
Because the connection is one-way, which removes three things every other Attribi CRM connection has. No write-back: no campaign, source or journey is written onto the Odoo lead or contact, so your sales team sees nothing from Attribi inside Odoo. No field provisioning: no custom fields are created, so there is nowhere for your website form to store an ad click id. And so no click-id recovery: matching is by email only.
Plainly: an Odoo-only stack fits if you want verified revenue on the dashboard and in your ad platforms, and does not fit if you want attribution inside Odoo. The write-back is on the roadmap, not in the product. The partial remedy is the tracker: with Attribi's script on your site the click id is captured at the visit, and the upload carries it when the order's email matches that visitor. The pixel-less attribution page sets out the trade.
The order, dates and amount here are illustrative; the field names and the payload are what the code sends.
On 3 September a search ad click lands with ?gclid= on the URL; the tracker records the visitor and the click id, and the quote form creates a lead with their email. On 24 September at 14:05 UTC the customer accepts quotation S00421. In Odoo that is a state change to sale: amount_total 4,800.00, currency_id [1, "EUR"], partner_id [812, "Acme GmbH"], write_date 2026-09-24 14:05:11.
At 04:00 UTC on 25 September the scheduled pull sends search_read on sale.order with the domain [["state","in",["sale","done"]],["write_date",">","2026-09-24 04:00:00"]]. S00421 comes back. The pull reads res.partner 812, gets sales@acme.example, matches it to the lead from 3 September, and marks it closed-won at 4,800 EUR, dated by the order's write_date rather than the time the pull ran. For Google Ads that is one Data Manager API event against the action Attribi created, Attribi - Closed Won, carrying eventTimestamp 2026-09-24T14:05:11Z, transactionId of the lead id plus closed_won, conversionValue 4800, currency EUR, the gclid and the hashed email. The transaction id is the lead and the stage, so a retried pull cannot credit S00421 twice. The click is 21 days old at upload: the conversion actions Attribi creates in Google Ads use a 30-day click-through window today, and Google will not credit a click-id conversion more than 90 days after the click (verified 2026-10-05). A change raising Attribi's window to 90 days is written and not yet merged; until then, a long quote-to-order cycle is the first thing to check when Odoo revenue shows in Attribi and not in Google.
amount_total as first read, tax and delivery included; later edits do not change the figure already uploaded.Do not connect Odoo if revenue is recognised anywhere other than a confirmed sales order; or alongside a Shopify store that mirrors orders into Odoo, unless you will reconcile the overlap by hand; or for in-Odoo attribution, which a one-way connection cannot give you. Connect it when the order is the record of the sale. CRM and revenue sources are on the Growth plan at 99 dollars a month, and every new workspace starts with 30 days on Pro, no card required.
Does Attribi work on the Odoo Standard plan? No. Odoo Online restricts external API access to the Custom plan; it is not available on One App Free or Standard, so a Standard subscription cannot be connected. Odoo.sh, Enterprise on-premise and self-hosted Community all work.
Does the Odoo connection still work on Odoo 19? Yes. Odoo 19 keeps the /jsonrpc endpoint Attribi uses, marked deprecated with removal scheduled for Odoo 22. Odoo 19 API keys are created with an expiry, and an expired key stops the pull until you paste a new one.
Why does a won opportunity in Odoo CRM send no revenue to my ad platforms? Because revenue is read from the sales order state, not the opportunity stage. A crm.lead in a Won stage has no confirmed order and no realised amount, and Attribi refuses to substitute the forecast expected_revenue figure. Confirm the quotation and the next pull picks it up.
What happens to an Odoo order priced in a custom currency? Its closed-won upload is withheld. Attribi checks the code against the ISO 4217 list, so a custom code such as PTS is unknown rather than uploaded as-is or relabelled as dollars. The order is retried on the next three pulls, then skipped with a message naming it.
Does a second order from the same Odoo customer upload a second conversion? No. Attribi fires one closed-won conversion per lead, and the Odoo pull checks whether the lead is already won before uploading. The second order is mirrored for reporting but not sent, unlike the Shopify connection, where a repeat purchase fires every time.
Odoo is the one Attribi CRM connection where the revenue figure is the one finance reports, because it comes from a confirmed order rather than a stage somebody dragged. The price is a one-way connection: nothing is written into Odoo, matching is by email, and an Odoo Online account needs the Custom plan. If your sales end in a confirmed order, connect Odoo and watch a month of orders reach your ad platforms on the 30 days of Pro every new workspace starts with.
Stages Attribi supports for Odoo.
Data source
Shopify multi-touch attribution: paid orders reach Attribi straight from your store with their real total, then upload to your ad platforms as revenue.
Ad platform
Google Ads offline conversion tracking: send qualified leads and closed-won revenue from your CRM, so your account sees CRM-verified outcomes, not just form fills.
Ad platform
Meta Conversions API for CRM: send qualified leads and closed-won revenue from your CRM to Meta server side, with hashed email, phone, and Instant Form lead IDs.
No. This connection reads and nothing else. There are no Attribi custom fields on your contacts or leads, no lead is ever created in Odoo, and no record is modified. That is a genuine difference from the HubSpot, Salesforce, Zoho and Pipedrive connections, where campaign data is written onto the record for your reps to see. If in-Odoo attribution matters to you, it is on the roadmap and is not in the product today.
By email address only. Attribi reads the contact linked to the sales order, takes the email from that contact record, and matches it against the person it already knows. Phone numbers are not used and neither is an ad click ID, because with no fields written into Odoo there is nowhere for a click ID to have been stored. That means match rates depend on your contacts carrying accurate email addresses, and installing the Attribi script is what adds click-level matching for Odoo customers.
A sales order that Odoo has moved to a confirmed or done state becomes a closed-won conversion carrying the order total and the order’s own currency. A CRM lead that enters one of the stages you nominated becomes a qualified conversion with no value on it. Quotations and draft orders are ignored entirely, and Odoo’s expected revenue field is never uploaded, because it is a forecast rather than money received.
Closed-won is unaffected, because it reads the order state rather than a stage name, so a renamed pipeline never stops revenue reporting. Qualified detection does use stage names, since that is how Odoo identifies CRM stages, so a rename there means updating the list in Attribi. Until you do, the qualified pass matches nothing rather than guessing at a replacement, which is deliberate: firing nothing is better than firing on the wrong stage.
On a schedule, and you can also trigger a read from the Connections page after a big close. Attribi tracks where it left off and only asks Odoo for records modified since then. If a run cannot finish enumerating, it deliberately does not move that marker forward, so the next run re-reads the same window rather than skipping orders. Duplicate reads are safe: an order that has already been counted is never counted twice.
Updated
Sign up and connect it from the Connections page.
Start FreeNo credit card · Free up to 500 visitors a month