Skip to Content

Odoo Integrations

Odoo Integration Architecture: Patterns and Trade-Offs

Most integration failures are not coding failures. They are two systems that were never told which one owns the data, connected by something that has no memory of what it already sent. Both problems are decided by architecture, before anyone writes a line.

Written by Tayyab RasheedOdoo 19 Certified, Technical Consultant

Part of our guide to Odoo Integrations

Decide ownership before anything else

Every field that exists in both systems needs one owner. Not a preference, a rule: which system is allowed to change it, and which one only reads it.

Get this wrong and the symptoms are miserable to diagnose. A price corrects itself overnight. A customer address reverts after an edit. Two systems overwrite each other on a loop, each behaving exactly as designed.

Write the ownership map before choosing any technology. One row per shared entity, one owner per row. It is a boring document and it prevents the single most expensive class of integration bug. A worked example, for a business selling through an online store:

EntitySystem of recordOdoo's roleWhy it lands there
Product catalogueOdooWritesPurchasing, costing and stock already live here
Web copy and imageryStorefrontReadsMerchandising changes daily and never touches the ledger
Available stockOdooWritesOne authoritative position, or two storefronts commit the same unit
OrderStorefrontReads, then owns fulfilmentThe customer transacted there, so that record is the original
Customer masterOdooWritesInvoicing, credit and history are accounting concerns
Shipment statusCarrier platformReadsOdoo cannot know more than the carrier does

Yours will look different. What it must not do is leave a row blank.

Where both sides genuinely need to write, split the field rather than sharing it. The webshop owns the web description, Odoo owns the internal one. Two fields with one owner each beats one field with two.

We rebuilt the multi-company model for a US retailer running several WooCommerce storefronts against one shared Odoo inventory. The stock boundaries were not enforced, so storefronts kept selling units already committed elsewhere. The repair was architectural rather than a better sync script: explicit centralised inventory rules, so every storefront reads and commits against one authoritative stock position instead of a stale local copy.

Four patterns

Direct API calls. Odoo talks to the other system, or the other system talks to Odoo, with nothing in between.

Middleware or an iPaaS. A platform sits between them, owning transformation, retries, queueing and monitoring so that you do not build those four things again for every connection.

File exchange. CSV or XML dropped somewhere on a schedule. Unfashionable and still correct for high-volume batch work, particularly with older systems and with partners who will not build an API for you.

Event-driven webhooks. Systems notify each other when something happens rather than asking repeatedly. Near real time, and every webhook sender will eventually drop a message, send one twice, and send two out of order.

Most real architectures mix these. Webhooks for urgent events, a nightly batch to reconcile what the webhooks missed. The batch is not redundant, it is what makes the webhooks survivable.

The decision matrix

PatternBest fitTrade-offFailure ownershipOdoo implementation implication
Direct APIOne or two connections, simple mappingsEvery retry, log and alert is code you writeYours, on both sidesA custom module: model methods for outbound, a controller for inbound
Middleware or iPaaSSeveral systems, or transformation belonging to neitherA licence, a vendor, and a third place to debugShared: it delivers, you still reconcileOdoo becomes one endpoint among several, so keep its logic thin
File exchangeHigh-volume batch, older systems, partners with no APILatency in hours, and no acknowledgement a row was appliedYours, at both ends of the dropA scheduled action reads or writes the file on a timetable
Event-driven webhooksLow-latency notice of discrete eventsDuplicates, out-of-order arrival and silent dropsThe receiver, alwaysInbound: an automation rule or a custom controller. Outbound: a server action, or your own queue

Read it as a cost question, not a quality one. None is better than the others; they put the cost in different places, and the right one puts it where you can carry it.

What Odoo gives you before you build anything

Some of this is configuration rather than code, and knowing what already ships changes what you should build.

The external API. Odoo exposes its models over HTTP. Historically that meant /xmlrpc, /xmlrpc/2 and /jsonrpc. In Odoo 19 all three are marked deprecated in the source and scheduled for removal in Odoo 22, and every call writes a deprecation warning to the server log. They still work. Odoo 19 adds /json/2/<model>/<method>, a POST endpoint authenticated by an Authorization: Bearer header carrying an API key, stateless and saving no session. Odoo 18 accepts bearer tokens on routes that ask for them, but the packaged endpoint is new in 19.

Authentication. API keys are scoped, so a key issued for RPC is only good for RPC. In Odoo 19 and 18 a key can carry an expiry, and the credential check refuses an expired key and one belonging to an archived user. In Odoo 19 a non-system user cannot create a key without an expiry.

Inbound events without code. An automation rule can use an On webhook trigger, publishing a URL of the form /web/hook/<uuid>: a public POST endpoint with CSRF disabled, a rotatable identifier, and a configurable expression that turns the payload into the record the rule acts on. Present in Odoo 17, 18 and 19.

Outbound events without code. A server action of type Send Webhook Notification posts chosen fields to a URL when it runs. Also in 17, 18 and 19, with a caveat covered below.

Scheduled actions. For anything batch, a cron record is the mechanism, and usually the right one.

Reach for a custom module when none of those fit, not before. Choosing between them, and reading a requirement closely enough to know which it needs, is most of the work on an Odoo integration project.

Real time is rarely the requirement

Real time sounds obviously better and is usually the expensive answer to a question nobody asked. Ask what actually depends on the delay. Stock on a busy webshop genuinely needs to be current, because the cost of overselling is real. A customer address does not, and neither does yesterday's invoice.

Batch is cheaper, easier to debug because you can inspect a whole run, easier to replay, and far kinder to rate limits. Use a schedule unless the business consequence of the delay justifies more.

Match on identifiers, not on names

Store the external system's identifier on the Odoo record. A field holding the Shopify customer id, the Stripe customer id, the marketplace order reference.

Then matching is exact. Without it, integrations match on name or email, and both are unstable: people change email addresses, names have three spellings, and two genuine customers can share one. Every duplicate-record problem in an integration traces back to matching on something that was never an identifier.

Odoo's own data model takes the same side. On product.product the internal reference is indexed and the barcode carries both an index and a uniqueness check, while the product name is a translatable field, so its stored value depends on the language the request runs in. A translatable label can legitimately differ between two reads of the same record.

Storing the external id also makes the sync idempotent: replaying a message updates the record it already created rather than creating a second one. That property is what lets you re-run a failed batch without fear.

Plan for failure, because it is normal

The other system will be down. Its certificate will expire. It will rate limit you mid-batch. A message will arrive twice, or out of order. Three things make that survivable.

Retry with backoff, not immediately and not forever. Immediate retries during an outage are indistinguishable from an attack.

A dead letter queue, or its equivalent: somewhere failed messages land so a human can see them. Failures that only exist in a log file are failures nobody knows about.

Monitoring that alerts on silence. The failure nobody catches is not the error, it is the sync that stopped running altogether. Alert on "no successful run in N hours", because that is the condition an error-only alert cannot see.

That last point is not theoretical in Odoo, and two built-in behaviours are worth knowing before you lean on them.

The outbound webhook server action is deliberately send-and-forget. It fires after the transaction commits, posts with a one second timeout, and on a timeout or an error writes a warning to the log and stops. There is no retry and no stored message, and that is the same in Odoo 19, 18 and 17. Sound design for a notification, and not a delivery guarantee, so if you need one that queue is yours to build.

Scheduled actions fail the opposite way. In Odoo 19 and 18 a failing cron is deactivated automatically and an administrator notified, but only once it has failed at least five times and has been failing for more than seven days, both conditions together. Odoo 17 has no such mechanism, so a broken job there keeps failing indefinitely. Either way the work has stopped, which is invisible to an alert that only watches for errors.

Where the code lives in Odoo

Integration logic belongs in the model layer, like any other business logic. Controllers receive and validate; models decide.

For outbound calls, do not make the HTTP request inside the user's save. A slow endpoint blocks the interface, and a failure rolls back their work. Queue it and let a scheduled action send it.

For inbound, a controller receiving a webhook should do the least possible: verify the signature, store the payload, return quickly. Senders time out and retry, so slow processing inside the request handler turns one message into several.

Odoo's own integrations are built this way. In the Stripe payment module the controller owns only the HTTP surface: it receives the notification, verifies the signature against a shared secret, refuses anything whose timestamp is too old, finds the matching transaction and hands off. Talking to the provider lives on the models, as methods on the provider and transaction records.

We built a custom payment provider module for a Gulf retail group on Odoo 18, integrating a regional gateway Odoo does not ship support for. That work is exactly the shape above: the payment flow, the callbacks and the transaction states, with the HTTP entry point thin and the decisions in the model.

The mapping document nobody writes

Before building, write down for every synchronised entity: the fields, the owner of each, the direction, the trigger, the matching identifier, and what happens on conflict.

It takes an afternoon. It is the artefact that makes the integration debuggable two years later, when the person who built it has moved on and the only remaining question is which side was supposed to win.

The specific ways this goes wrong in practice are collected in integration mistakes.

FAQ

Questions, answered.

Should we integrate Odoo directly or use middleware?

Direct is right for one or two integrations with simple mappings. Middleware earns its cost once you have several systems, need retries and monitoring you did not build, or need transformation logic that does not belong in either system. The deciding factor is usually the number of connections, not their difficulty.

What is the most important decision in an Odoo integration?

Which system owns each piece of data. Without a single owner per field, both sides eventually write the same record and the last writer wins at random. Almost every long-running sync problem traces back to this being left implicit.

How do we stop an integration creating duplicate records?

Store the external system's identifier on the Odoo record and match on it, rather than matching on name or email. Then a repeated message updates the existing record instead of creating a second one, which also makes the sync safe to replay.

Is XML-RPC still supported in Odoo 19?

It still works, but it is on notice. In Odoo 19 the /xmlrpc, /xmlrpc/2 and /jsonrpc endpoints are marked deprecated in the source and scheduled for removal in Odoo 22, and every call to them writes a deprecation warning to the server log. Odoo 19 adds a replacement under /json/2, a POST route addressed by model and method and authenticated with an Authorization Bearer header carrying an API key. New integrations built on Odoo 19 should target the new endpoint. Existing ones have time, but not indefinitely.

With CODEerts

Want your systems talking properly?

We are certified Odoo partners. We connect Odoo to stores, payment providers, messaging and reporting tools, and we maintain those connections through upgrades.

See how we can helpBook a call