Odoo Gives Your Integration One API Call per Second. Everything Else Follows From That.
Search for Odoo integration advice and you get two kinds of page: a partner brochure, or a tutorial that shows you how to call search_read from Python. Neither one tells you the thing that actually decides your architecture. Odoo publishes hard limits on what an external integration is allowed to do, and two of those limits have dates attached to them.
This is written from the build side, for the team that has to connect a product, a customer portal, a shop, a pricing engine or an internal tool to an Odoo database and keep it working through Odoo's release train. It assumes Odoo stays. Nothing here is an argument for replacing an ERP that already runs your accounting. The only question worth answering is where the boundary sits between Odoo and the software you own, and Odoo's own documentation answers most of it if you read the right five pages.
The five limits, in one table
| Limit | What Odoo states | What it forces on your design |
|---|---|---|
| Plan gate | External API access is available on Custom pricing plans, and not on One App Free or Standard | The API is a line item in the ERP contract, not a free capability. Confirm it before you scope the integration |
| Call budget | Roughly one call per second, unsustained, with no parallel calls, is what the acceptable use policy calls acceptable on Odoo Cloud | Batch reads, aggregate server-side, and let Odoo push events to you instead of polling it |
| Transaction boundary | Every call to the JSON-2 endpoint runs in its own SQL transaction, and calls cannot be chained inside one transaction | Any multi-step operation that must be all-or-nothing belongs in a single method inside a custom module |
| Protocol clock | /xmlrpc, /xmlrpc/2 and /jsonrpc are scheduled for removal in Odoo 22 (fall 2028) and Odoo Online 21.1 (winter 2027) | New work targets /json/2. Existing RPC clients need a dated migration plan, not a backlog ticket |
| Upgrade clock | Each major version is supported for three years, Odoo Online upgrades are mandatory on a schedule, and a database with custom modules cannot upgrade until those modules are compatible | Custom code is a recurring maintenance liability with someone's name on it, and your integration tests are part of the upgrade gate |
Read the last two rows together, because they interact in the way that hurts. Your RPC client has to be gone by the time Odoo Online reaches 21.1, and the upgrade that takes you there is an upgrade you do not fully control.
Limit 1: the external API is a plan feature
The first surprise is commercial, not technical. Odoo's JSON-2 API reference states that access to data via the external API is only available on Custom pricing plans, and that it is not available on One App Free or Standard plans.
That single note reorders a lot of projects. A company that picked the Standard plan because it looked like the sensible middle option has bought an ERP that its own software cannot talk to. The integration budget then has to absorb a plan change for every user, every month, forever. It is the cheapest thing to verify and the most expensive thing to discover in week six.
Two practical notes. First, self-hosted Odoo Community has no plan gate at all, because there is no plan, which is one of the few real arguments for running it yourself. Second, plan names and tiers are marketing surface and they move, so treat the documentation note as the reason to check your own contract rather than as a quote to paste into a decision memo.
Limit 2: one call per second, and no parallel calls
Odoo does not publish a per-endpoint quota with a documented 429 threshold. What it publishes is a policy. The acceptable use policy for Odoo Cloud lists unthrottled RPC and API calls as prohibited network abuse, notes that batch APIs exist for imports, and says that throttled calls are typically acceptable for unsustained usage at a rate of one call per second, with no parallel calls. It also names the escape hatch: on Odoo.sh, dedicated hosting can be considered as an alternative to lift that restriction.
A policy ceiling is harder to design against than a documented quota, because you cannot measure your way to safety. You have to assume the number is the number. One call per second, serial, is a budget of about 86,000 calls a day if you use every second, which no real integration does. So the design moves from "how fast can we go" to "how few calls does this need".
What that buys you, in call terms
- One round trip instead of two.
search_readreplaces asearchfollowed by aread. Every combined ORM method is a call you did not spend. - Aggregate inside Odoo.
read_groupreturns grouped sums from the database. Pulling 40,000 order lines to total them in your own code is the same answer at a hundred times the cost. - Write in batches. Passing a list of ids to one write is one call. A loop over ids is one call per record, and a loop of 3,000 records is an outage you caused.
- Ask for the fields you need. A
fieldslist keeps the response small and stops your client from depending on columns you never wanted, which matters again at upgrade time. - Never poll for change detection. Polling is the single largest consumer of a one-per-second budget, and it is the one that scales with your customer count rather than your data volume.
The replacement for polling is already in the product. Odoo's automation rules include a Send Webhook Notification action that posts the selected fields of a record to a URL you specify, with a sample payload preview, and they can also be triggered by an inbound webhook from an external system. Odoo's own documentation attaches a warning to this: get a developer or solution architect involved, because a badly configured webhook can disrupt the database. That warning is correct and it is the reason webhooks belong behind a queue on your side, not wired straight into business logic.
The shape that survives is boring. Odoo pushes an event. Your service accepts it, writes it to your own store, returns 200 fast, and does the real work asynchronously with a retry policy and a rate limiter that respects one call per second. Your product reads from your store, not from Odoo. Odoo stays the system of record for the things it owns.
An Odoo database in the middle of a product roadmap and nobody owns the boundary?
Scope the integration workLimit 3: every call is its own transaction
This is the limit that produces the expensive class of bug, and it is stated plainly in the transaction section of the JSON-2 reference: all calls to the JSON-2 endpoint run in their own SQL transaction, committed on success and discarded on error, and it is not possible to chain multiple calls inside a single transaction. The documentation goes further and says the database might be modified by other concurrent transactions between your calls, and that this is especially dangerous for operations related to reservations and payments.
Read that as an architectural instruction, because it is one. Odoo's recommended fix is in the same paragraph: always call a single method that performs all the related operations in a single transaction, and where no such method exists, create one in a dedicated module.
Which means the honest answer to "can we do this without touching Odoo code" is often no. A three-call sequence that checks stock, reserves it, and confirms an order is not a transaction. It is three transactions with two windows in which another user, another integration or a scheduled action can change the world underneath you. The failure is not a crash. It is a double reservation, a payment against a stale price, or an order line that exists without its parent, discovered by accounting a month later.
Three rules we apply to every Odoo write path
- One business operation, one call. If the operation has an invariant, it gets a method in a custom module and your service calls that one method. The invariant lives in the database transaction, not in your orchestration code.
- Idempotency keys on everything that creates. Your retry policy will re-send a call whose response you never saw. Store your own key on the Odoo record, check it before creating, and a retry becomes a no-op instead of a duplicate invoice.
- Reconcile, do not trust. A nightly job that compares your store against Odoo on a small number of aggregate reads catches the drift that per-call error handling misses. Cheap in calls, and it is the only thing that finds the failures nobody logged.
Limit 4: the RPC protocol has a removal date
Almost every existing Odoo integration in the wild speaks XML-RPC, because for many years that was the answer. The External RPC API reference now opens with a removal notice: both the XML-RPC and JSON-RPC APIs at /xmlrpc, /xmlrpc/2 and /jsonrpc are scheduled for removal in Odoo 22 (fall 2028) and Odoo Online 21.1 (winter 2027), with the External JSON-2 API as the replacement. All three exposed services, common, db and object, are deprecated. Internal controllers declared with @route(type='jsonrpc') are explicitly not covered by that notice.
Two dates, and the one that binds you depends on where your database lives. Odoo Online reaches the cutoff first, in winter 2027, because Odoo Online ships minor versions and moves faster than the major release line. Self-hosted and Odoo.sh databases have until Odoo 22 in fall 2028, plus however long you are prepared to sit on an unsupported version.
The migration itself is small if your call layer is one module and painful if RPC calls are scattered through your codebase. What changes:
| Concern | XML-RPC and JSON-RPC | External JSON-2 |
|---|---|---|
| Endpoint | /xmlrpc/2/common then /xmlrpc/2/object | POST /json/2/<model>/<method> |
| Authentication | Log in first, carry the returned user id into every subsequent call | An API key as a bearer token in the Authorization header, no login round trip |
| Database selection | A positional argument in every call | The optional X-Odoo-Database header, needed when one server hosts several databases |
| Payload | Positional arguments in a fixed order | A JSON object with ids, context and named parameters |
| Discovery | Read the source of the model you are calling | A per-database documentation route at /doc, generated from that database's own models |
The authentication change is the one worth planning around rather than porting mechanically. JSON-2 keys can be created, revoked and rotated programmatically through res.users.apikeys, with an expiration date that is validated against the maximum key duration allowed by the user's roles. Odoo's guidance is to run automated integrations under a dedicated bot user with the minimum permissions and an empty password, so the login route is closed and the access log names the bot rather than impersonating a person. Do that once, per integration, and key rotation becomes an ops task instead of a migration.
Limit 5: the upgrade clock belongs to Odoo
The upgrade documentation is the page most integration plans skip, and it contains the constraint with the longest tail. Each major version is supported for three years. On Odoo Online, an upgrade is mandatory every two years for a major version, and a few weeks after the next release for a minor version, with minor versions arriving roughly every two months. Odoo's Upgrade Team runs a silent test upgrade of every database that is due, and if that test succeeds and takes under twenty minutes, the automatic upgrade proceeds unless you act before the deadline.
Then the two sentences that decide who carries the cost. A database that contains custom modules cannot be upgraded until a version of those modules exists for the target Odoo version. And if a change in a new version breaks a customization, making it compatible is the responsibility of the maintainer of that custom module. Odoo's own testing checklist puts integrations with external software, APIs and EDI first among the things you must test.
That is not a complaint about Odoo. It is the deal, and it is a reasonable deal. But it means every custom module and every integration you build has a recurring cost with a name attached, and the name has to be a real person or a real contract. Version 19 makes the point concrete: res.users.groups_id became group_ids in the 19.0 source, and the separate contract model moved into HR core as hr.version. Any external client that named those fields returns a clean 200 with the wrong shape, or an error, depending on the call. Neither is caught by a test suite that mocks Odoo.
The cheap defence is a contract test: a small suite that runs your real read and write paths against an upgraded copy of the database and asserts on the fields you actually depend on. It is the same discipline as any other API you do not control, and it turns the mandatory upgrade window from a fire drill into a morning.
Where hosting decides the argument
Three of the five limits are set by where the database runs, which makes hosting an architectural decision rather than a procurement one.
| Option | Call budget and API access | Upgrade control | Fits when |
|---|---|---|---|
| Odoo Online | Acceptable use ceiling applies, external API tied to the plan tier | Rolling Release, mandatory upgrades on Odoo's schedule, RPC endpoints gone by 21.1 in winter 2027 | Standard processes, light integration, no appetite for infrastructure |
| Odoo.sh | Same policy, with dedicated hosting available as the documented way to lift the throttling restriction | Staged branches and test builds, upgrades on your trigger within the support window | Real custom modules, a CI habit, integration traffic that a shared ceiling would throttle |
| Self-hosted Community | No plan gate and no policy ceiling, you own the capacity and the reverse proxy | Entirely yours, including the decision to fall out of support | Heavy integration, data residency requirements, in-house ops that already exists |
One licensing fact matters here and is regularly guessed at. The Odoo Community source ships under the LGPL version 3, which is why a proprietary custom module on top of Community is a normal thing to do rather than a legal argument. Enterprise editions and Odoo's own hosted plans carry separate commercial terms, so read those against your own case instead of inheriting an opinion from a forum thread.
Self-hosting is not free. You are buying the removal of a rate ceiling with the addition of backups, upgrades, monitoring, a reverse proxy you now rate-limit yourself, and someone on call. That trade is the same one described in our guide to custom software versus off-the-shelf: pay for the thing your business logic actually touches, configure the rest.
The reference shape we build
Every Odoo integration we have looked at converges on the same architecture once the five limits are on the table. It is one service, owned by your team, sitting between Odoo and everything else.
- An anti-corruption layer. One module in your codebase knows Odoo's model names, field names and quirks. Nothing else in your product imports it. When
groups_idbecomesgroup_ids, you change one file. - Your own read store. Whatever your product reads on the hot path lives in your database, populated by events and reconciled on a schedule. Product latency stops depending on an ERP under a call ceiling.
- An inbound webhook endpoint with a queue. Accept, persist, return 200, process asynchronously, retry with backoff. Odoo's automation rule is the producer and your queue absorbs bursts that a synchronous handler would drop.
- An outbound writer with one lane. A single serialized worker enforces the call budget in one place, carries idempotency keys, and gives you one dashboard for the whole integration's call volume.
- Custom module methods for invariants. Small, named, transaction-scoped methods for the operations that must not tear. Reviewed and versioned like product code, because at upgrade time they are your liability.
- Contract tests against a real upgraded database. The gate that makes Odoo's upgrade schedule survivable.
The closest analogue in our own work is not an ERP at all. On the MyMerch process automation engagement the client ran a working commerce operation across many storefronts and was losing margin in two specific places. The tempting proposal was a rewrite. What actually worked was naming the manual handoffs, automating those, and leaving the platform alone. The same instinct applies to an ERP: the integration boundary is where the value is, and the platform underneath it is usually fine. We wrote up the general version of that argument in architecture remediation without a rewrite.
Four things we would not do
- Do not mirror Odoo in real time. A two-way sync of shared mutable state across a one-per-second link is a distributed systems project wearing an integration costume. Pick a writer per field and keep it.
- Do not move core accounting into your product. The moment invoices, tax logic or the chart of accounts live in two systems, you own reconciliation forever. If a statutory obligation is pushing you there, the answer is usually a layer beside the ERP rather than inside your app, which is the same conclusion we reached about structured e-invoicing around a payment processor.
- Do not put business logic in automation rules. Server actions and code blocks in the UI are invisible to code review, untested, and unversioned. They are excellent glue and a terrible place for rules that money depends on.
- Do not let the integration be nobody's module. Undocumented, unowned Odoo customizations are the most common reason an upgrade stalls, and the most common thing a new provider inherits blind. Our checklist for that handover situation is in taking over a software project from another provider.
A 30-day plan to get from unknown to safe
- Days 1 to 3, establish the facts. Hosting type, exact version, plan tier and whether the external API is contractually available. Then list every existing integration and which protocol it speaks. Most teams cannot answer the last part on day one.
- Days 4 to 10, measure the call volume. Log every outbound call for a week, grouped by caller and purpose. The polling jobs will be the top of that list and the first thing to delete.
- Days 11 to 18, name the invariants. Write down every operation that must be all-or-nothing. Each one becomes a single custom module method with a test, or an accepted risk with a reconciliation job behind it.
- Days 19 to 25, build the boundary. One anti-corruption module, one serialized writer with idempotency keys, one webhook endpoint with a queue. No new features in this window.
- Days 26 to 30, plan the JSON-2 cutover. A dated plan tied to your own upgrade window, not to 2027. A dedicated bot user, per-integration API keys with expiry, and a rotation runbook.
Thirty days assumes someone owns the decisions. Where that owner does not exist, the work stalls at step two, which is a common reason companies bring in a fractional CTO for a bounded integration programme instead of hiring for it.
Odoo integration FAQ
Is XML-RPC deprecated in Odoo?
What is the Odoo API rate limit?
Do I need a paid Odoo plan to use the external API?
Can I run several Odoo API calls in one transaction?
Should I use webhooks or polling with Odoo?
Do custom modules block an Odoo upgrade?
How often must an Odoo Online database be upgraded?
What broke for integrations in Odoo 19?
Can I keep a proprietary custom module on Odoo Community?
Final thoughts
Odoo is a good ERP with a deliberately narrow external contract, and it documents that contract honestly. The teams that struggle are the ones that treat it as an open database with a REST API bolted on. Read the five limits as design inputs: budget your calls, put invariants in one transaction, move to JSON-2 on your own schedule rather than in 2027, and give every custom module an owner who will still be there at the next upgrade. Build one boundary service you control, and the ERP becomes a dependency you manage instead of a deadline you react to.
Primary sources
- Odoo 19 documentation, External JSON-2 API, including the plan restriction, API key management and the transaction section
- Odoo 19 documentation, External RPC API, with the removal notice for the XML-RPC and JSON-RPC endpoints
- Odoo acceptable use policy, network and services abuse, on throttled call rates
- Odoo 19 documentation, upgrade, on support duration, mandatory upgrades, Rolling Release and custom module responsibility
- Odoo 19 documentation, automation rules, including the Send Webhook Notification action
- Odoo 19.0 source, res.users, showing the group_ids field
- Odoo 19.0 source, the hr.version model in HR core
- Odoo 19.0 LICENSE file, LGPL version 3 for the Community source
