CRM

Odoo marketing attribution, read from confirmed orders

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

What Attribi does with Odoo

Confirmed sales order revenue

Imports

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.

Qualified from your CRM stages

Imports

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.

No forecast is ever uploaded as revenue

Imports

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.

Odoo Online, Odoo.sh, or self-hosted

Imports

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.

On the roadmap

Planned, not live today. Listed so you can see what this integration does not do yet, before you sign up.

Attribution fields written back into Odoo

Planned Sends

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.

Setting up Odoo

  1. 1

    Check your Odoo plan first

    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.

  2. 2

    Create an API key and connect

    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.

  3. 3

    Grant the right reads, then name your stages

    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

Why Odoo attribution starts at the sales order, not the opportunity

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.

How does Odoo marketing attribution work in Attribi?

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 modelFilter Attribi appliesFields readWhat it becomes
sale.orderstate in (sale, done), write_date after the last pullid, name, amount_total, currency_id, partner_id, write_date, date_orderClosed-won, value = amount_total
res.partnerpartner_id values from one page of orders, one batched readid, emailThe email the order is matched on
crm.leadstage_id.name in your qualified list, write_date after the last pullid, name, email_from, stage_id, write_dateQualified, no value
crm.stageNone, a listing ordered by sequencenameThe stage picker at connect time

Data path from Odoo to Attribi to the ad platforms. sale.order with state sale becomes a closed-won conversion with amount_total and currency, matched through partner_id to the res.partner email. crm.lead in a named stage becomes a qualified conversion with no value. Nothing flows back into Odoo.

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.

Does that suit a lead-generation business or a store?

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.

Is the connection XML-RPC, JSON-RPC, or the new JSON-2 API?

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.

Odoo Online, Odoo.sh or self-hosted: what changes?

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.

What happens to an order with no usable currency?

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.

Can Shopify and Odoo both count the same sale?

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.

Why is "Odoo as the only CRM" a red flag?

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.

A worked example: from a confirmed order to a Google Ads upload

Illustrative timeline: a Google Ads click on 3 September, quotation S00421 confirmed in Odoo on 24 September at 4800.00 EUR, and the daily pull on 25 September matching the lead by email and uploading a closed-won event with the transaction id, value, currency, gclid and hashed email, 21 days after the click.

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.

The limits

  • Reads are daily, plus a manual pull from the Connections page; a sale confirmed this morning reaches your ad platforms tomorrow.
  • Matching is by email only. A contact with no email, or one that differs from the email on your form, is mirrored and never matched.
  • One closed-won upload per lead. A second order for the same contact is mirrored but not uploaded, so repeat-customer value is undercounted.
  • The value is amount_total as first read, tax and delivery included; later edits do not change the figure already uploaded.
  • A contact first seen in Odoo does get a lead-stage conversion, but only when the pull discovers it, up to a day later.
  • Orders in several currencies upload in each currency, and the dashboard will not blend them into one ROAS figure.

When not to use this route

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.

Common questions about this connection

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.

The takeaway

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.

Funnel stages

Stages Attribi supports for Odoo.

  • Qualified
  • Closed Won

Works with

Frequently asked questions

Does Attribi write anything into our Odoo?

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.

How does Attribi match an Odoo order back to a visitor?

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.

Which Odoo records become which conversions?

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.

What if we renamed our CRM stages?

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.

How often does Attribi read from Odoo?

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

Connect Odoo to Attribi.

Sign up and connect it from the Connections page.

Start Free

No credit card · Free up to 500 visitors a month