# Odoo Salesforce integration

*Odoo → Salesforce*

**In short:** An Odoo to Salesforce integration keeps your ERP and your CRM describing the same customer at the same time. Odoo partners, sales orders, invoices, products, and delivery status are matched to Salesforce Accounts, Contacts, Orders, and price book entries by stable external IDs and upserted through the Salesforce API, while Salesforce keeps the sales-facing view of fulfilment and billing that Odoo owns. Done properly it is not a nightly CSV push: it is an idempotent, delta-driven pipeline that respects Salesforce API limits, multi-currency, and referential order so nobody re-keys a won deal into the ERP.

## What an Odoo to Salesforce integration actually does

Salesforce is where your sales team lives: leads, opportunities, forecasts, the story of how a deal was won. Odoo is where that deal becomes real: the confirmed sales order, the stock reservation, the delivery, the customer invoice, the payment. The moment a deal closes, the same customer exists in two systems that were never designed to agree with each other.

An Odoo to Salesforce integration closes that gap automatically. It keeps the customer master aligned, pushes the operational truth Odoo owns - order confirmed, goods shipped, invoice raised, balance outstanding - back into Salesforce where account managers can see it, and does it without anyone exporting a spreadsheet or re-typing a won opportunity into the ERP. Sales stops asking operations "did that order actually ship", because Salesforce already shows it.

## What data moves

| Odoo object / event | Becomes in Salesforce | Notes |
| --- | --- | --- |
| Company partner (`res.partner`, is_company) | Account | Matched by a stable External ID; upserted, never blind-inserted |
| Individual partner / contact | Contact linked to its Account | Parent Account must exist first, so ordering matters |
| Confirmed sales order (`sale.order`) | Order (or Opportunity update) | Only on state = sale; draft quotations are not pushed |
| Order lines (`sale.order.line`) | OrderItem / OpportunityLineItem | Requires a matching Product2 and PricebookEntry to exist |
| Product (`product.product`) | Product2 + PricebookEntry | Price book and currency mapped from the Odoo pricelist |
| Customer invoice (`account.move`) | Invoice / custom billing object | Status and amount due surfaced for account visibility |
| Delivery (`stock.picking`) | Order fulfilment status field | Sales sees shipped / partially shipped without ERP access |

The exact field mapping, the External ID fields, and which side wins on a conflict are agreed once during design and encoded in the pipeline. After that, nobody maps a field by hand again.

## The details that break naive syncs

A point-and-click connector or a nightly CSV push gets you a demo that works and a production system that drifts. The expensive parts are specific to this pair:

- **Identity and matching.** Odoo keys records by an integer database ID that is meaningless in Salesforce, and Salesforce uses its own 15 or 18-character IDs. Neither is a business key. You need a dedicated External ID field on the Salesforce side and a stored cross-reference so an update lands on the right record instead of creating a duplicate. Salesforce upsert exists precisely for this and a naive sync ignores it.
- **Referential order.** A Contact cannot be written before its Account, an OrderItem cannot exist without a Product2 and a PricebookEntry, and an Order needs its Account. Push objects in the wrong order and Salesforce rejects half the batch. The pipeline has to sequence writes to respect these dependencies.
- **Odoo has no webhooks.** Odoo does not emit events the way Salesforce does. You read changes over XML-RPC or JSON-RPC and use `write_date` as a delta marker, which means handling records touched by a bulk edit, an import, or a scheduled action - not just user clicks.
- **Currency and price books.** Odoo pricelists and `res.currency` do not map one-to-one onto Salesforce multi-currency and PricebookEntries. Amounts have to arrive in the right currency with the right rounding, or an opportunity value in Salesforce silently disagrees with the order in Odoo.
- **API limits and volume.** Salesforce enforces a daily API call budget. One REST call per record works in a demo and dies at scale. Volume belongs on the Bulk API 2.0, with backoff on limit errors and partial-success handling so one bad record does not fail the whole load.
- **Partial failures.** Salesforce Bulk jobs return per-record success. A pipeline has to capture the rows that failed a validation rule or duplicate rule, retry the transient ones, and surface the rest - not silently drop them.

## How we build and run it

We treat this as a pipeline, not a scheduled export. Changed Odoo records are read on a schedule via RPC using `write_date` deltas, validated, transformed into your agreed Salesforce shape, and upserted through the REST or Bulk API by their External ID. Where Salesforce is the source for a given object, Change Data Capture drives the flow the other way. Writes are sequenced so parents exist before children.

The pipeline is idempotent: every record carries a stable identifier and is upserted, so a retry or a re-run updates in place and never spawns a duplicate Account or a second Order. It runs on cloud-native, fully EU-hosted AWS infrastructure, so partner and order data never leaves the EU, which keeps your DPA / AVV and GDPR obligations clean.

Then we keep it running. Monitoring, alerting, incident response, and - critically - watching for Odoo upgrade changes and Salesforce API version deprecations are our responsibility under contract. You get a named owner and an SLA. Revenue operations stops depending on a middleware flow that only one person understood.

## When this integration is worth building

If a handful of deals a month cross from Salesforce into Odoo and someone re-keys them in five minutes, that manual step is genuinely fine and we will tell you so. A native connector app may also be enough when your objects are standard and your volume is low.

The integration earns its place when deal volume makes re-keying a real cost, when duplicate Accounts and stale fulfilment status start eroding trust in Salesforce, when multi-currency or multi-company Odoo makes the mapping non-trivial, or when you need the whole thing owned and operated by someone accountable rather than maintained by whoever set up the last automation.

## Frequently asked questions

### Which system should own which data, Odoo or Salesforce?

Usually Salesforce owns the pipeline (leads, opportunities, sales activity) and Odoo owns everything after the deal is won - the confirmed sales order, inventory, invoicing, and fulfilment. The customer master (partner versus Account) is the one record both sides touch, so we agree a single source of truth for it up front and let the other system receive updates. Getting that ownership map right is most of the design work.

### Does Odoo support webhooks, or do you have to poll it?

Odoo has no general-purpose outbound webhook system. Recent versions add limited webhook actions, but in practice we read changes from Odoo over its XML-RPC or JSON-RPC API using the write_date field as a delta marker, and we push into Salesforce through its REST and Bulk APIs. Salesforce can notify us the other way via Change Data Capture or Platform Events. We pick per object rather than forcing one mechanism on both systems.

### How do you stop duplicate Accounts and Contacts appearing in Salesforce?

Every Odoo record is matched to Salesforce by a stable external ID stored in a dedicated External ID field, and we write through Salesforce upsert rather than insert. That means a re-run updates the existing Account instead of creating a second one. Where the two systems already hold overlapping customers, we run a one-time reconciliation against your existing Salesforce duplicate rules before the pipeline goes live.

### Will this blow through our Salesforce API limits?

It will not, because we design for them. High-volume writes go through the Salesforce Bulk API 2.0 rather than one REST call per record, deltas keep the volume proportional to what actually changed rather than a full reload, and the pipeline backs off and retries on limit errors instead of failing the batch. We size the expected daily call budget with you before build.

### Who operates it after go-live?

We do. The pipeline runs on cloud-native, fully EU-hosted infrastructure that we monitor. If Odoo changes its RPC behaviour on an upgrade or Salesforce deprecates an API version, that is our problem to fix under contract, not a surprise your revenue operations team discovers when orders stop flowing. You get a named owner, alerting, and an SLA rather than a middleware recipe nobody maintains.

## Related integrations

- [Dynamics 365 ↔ Salesforce](https://seamless.engineering/integrations/dynamics-365-salesforce/): Dynamics 365 Salesforce integration
- [NetSuite ↔ Salesforce](https://seamless.engineering/integrations/netsuite-salesforce/): NetSuite Salesforce integration
- [Sage ↔ Salesforce](https://seamless.engineering/integrations/sage-salesforce/): Sage Salesforce integration
- [SAP ↔ Salesforce](https://seamless.engineering/integrations/sap-salesforce/): SAP Salesforce integration
- [Airtable ↔ Salesforce](https://seamless.engineering/integrations/airtable-salesforce/): Airtable Salesforce integration
- [HubSpot ↔ Salesforce](https://seamless.engineering/integrations/hubspot-salesforce/): HubSpot Salesforce integration

## Browse by system

- [All Salesforce integrations](https://seamless.engineering/integrations/salesforce/)
- [Salesforce API changelog](https://seamless.engineering/api-changelog/salesforce/): No breaking changes or deprecations in the last 90 days

## Request a scoping call

Need this integration built and permanently operated? Tell us which systems connect and what data has to move. Fixed-price scoping quote within 48 hours.

- Email: hello@seamless.engineering
- Contact form: https://seamless.engineering/#contact
