Skip to content
OdooWebApps.com

How Odoo's API actually works (JSON-2, JSON-RPC, XML-RPC and the ORM)

By Tal Oz, OdooWebApps. Published 18 September 2026, updated 22 September 2026.

What protocols does Odoo's API actually use?

Three, depending on the version. Odoo 16, 17 and 18 expose the external API over two RPC protocols, XML-RPC (at /xmlrpc/2/object) and JSON-RPC (at /jsonrpc). They are not two feature sets; both are transports for calling the same ORM methods through one generic dispatcher, execute_kw. Odoo 19 adds a third, JSON-2: a plain HTTP API where you POST a JSON body to /json/2/<model>/<method>. The RPC endpoints still work on 19 but are deprecated and scheduled for removal in Odoo 22 (fall 2028). New integrations on Odoo 19 should use JSON-2; on older versions, JSON-RPC is the easier of the two to debug.

# Odoo 19, JSON-2
POST /json/2/sale.order/search_read
Authorization: bearer <api key>
Content-Type: application/json
{"domain": [["state", "=", "sale"]], "fields": ["name", "amount_total"], "limit": 5}

# Odoo 16 to 18 (and still 19), JSON-RPC
POST /jsonrpc
{"jsonrpc": "2.0", "method": "call", "params": {"service": "object", "method": "execute_kw",
 "args": ["<db>", <uid>, "<api key>", "sale.order", "search_read",
          [[["state", "=", "sale"]]], {"fields": ["name", "amount_total"], "limit": 5}]}}

How does authentication work?

With JSON-2 the API key goes in an Authorization: bearer header on every request; there is no login step and no user id to pass. The key is created from the user's Preferences under Account Security, and Odoo 19 caps its lifetime at three months, so a production integration needs a rotation routine (keys can also be generated programmatically through res.users.apikeys/generate). With the RPC protocols a client first calls authenticate with the database name, login and either a password or an API key, gets a user id back, and passes the database, user id and key on every call after that. In both cases the API key is the safer choice over a password: it can be revoked on its own and scoped to RPC use only. On Odoo Online, note that the external API is only available on the Custom plan.

What can you actually call once authenticated?

Nearly everything the ORM itself supports, through a single generic dispatch method (commonly called execute_kw) that takes a model name, a method name, and its arguments. Standard CRUD methods are always available: search, search_read, read, create, write, unlink, and so is any other public method defined on that model, including business-logic methods like confirming a sales order or validating a stock transfer, which matters because those methods run the same side effects (state changes, related record updates) that clicking the equivalent button in Odoo's own interface would trigger.

What does a typical call actually look like?

Reading data is usually done with search_read, which takes a domain (a list of filter conditions, like [["state", "=", "sale"]]) and a list of fields to return, combining what would otherwise be a separate search-then-read round trip into one call. Aggregated numbers (totals, counts grouped by a field) generally go through read_group instead of pulling every record back and summing client-side. Writing data is a create or write call with a dictionary of field values, or a call to a specific method when the change needs to trigger Odoo's own business logic rather than just set a field.

Does the API respect Odoo's normal permissions?

Yes, entirely. Every call runs as the authenticated user, subject to that user's model-level access rights and row-level record rules exactly as if they were logged into the Odoo web client. There's no separate, more permissive "API mode," which is also why the right way to scope an integration is a dedicated API user with only the access it needs, not the main admin account.

What are the practical gotchas worth knowing up front?

A few things trip up integrations that weren't built with Odoo's specifics in mind: many2one fields come back as a two-item list ([id, display_name]) rather than a plain id; all datetimes are stored and returned in UTC regardless of the user's timezone, so conversion is the caller's job; and calling search in a loop followed by individual read calls per record ("N+1" calls) is a common, avoidable performance mistake when search_read would do it in one round trip. Field names and available methods can also shift slightly between major Odoo versions, which is worth knowing before assuming an integration will need zero maintenance forever. See the guide on Odoo upgrades linked below.

Want this looked at for your setup?

Every Odoo instance is configured a little differently. Tell us what you're working with and we'll give you a straight answer.