← Blog
odooapiendpointsjson-rpcexternal-api

Odoo API Endpoints: The One Route You Actually Call

Searching for a table of Odoo API endpoints? There isn't one — the External API is a single method behind a handful of routes. Here is the real map.

ODXProxy Team · Jul 8, 2026 · 8 min read

Odoo API Endpoints: The One Route You Actually Call — ODXProxy blog cover

If you came here looking for a list of Odoo API endpoints — a REST table with GET /partners, POST /orders, and one route per model — the honest answer is that it does not exist, and that is good news. Odoo does not expose a sprawling REST surface you have to learn model by model. Its External API is a single universal method behind a handful of endpoints, and once you know that method you can reach every model in the database. This guide maps the endpoints that actually exist, explains why there are so few of them, and shows the one route you will spend all your time calling.

Why there is no endpoint per model

Most APIs give you a resource tree: an endpoint for customers, another for invoices, another for products. Odoo takes the opposite approach. It exposes one server-side method, execute_kw, that sits in front of the entire ORM. Every operation — reading partners, posting an invoice, creating a sale order — is the same call with a different model name and method name.

That is why "list all the Odoo API endpoints" is the wrong question. The thing that varies between operations is not the URL; it is the model and the method you pass as arguments. Change those two values and you have a different operation, over the same endpoint. There is no route to version, no resource tree to memorize, and nothing to build server-side before you can call a model.

Odoo's own endpoints: the short list

Talking to Odoo directly, the External API lives behind just a few routes:

  • /xmlrpc/2/common — the XML-RPC endpoint for session-level calls like authenticate, which exchanges your database, login, and API key for a numeric user id (uid).
  • /xmlrpc/2/object — the XML-RPC endpoint that exposes execute_kw, i.e. the actual data calls.
  • /jsonrpc — the JSON-RPC 2.0 endpoint. It reaches the same execute_kw with a JSON payload instead of XML, and it is the cleaner choice for anything new.
  • /web/session/authenticate — the session-cookie login used by the web client; occasionally used by integrations that want a session rather than an API key.

That is essentially the whole map. Whether you go over XML-RPC or JSON-RPC, you are calling the same execute_kw underneath — the transport is just how the request is serialized on the wire. If you want the transport comparison in depth, see The Odoo External API: a JSON-RPC integration guide.

XML-RPC and JSON-RPC are two encodings of the same External API. Picking one does not change which models or methods you can reach — only how the request is framed.

The one endpoint you call through a proxy

Talking to those endpoints directly means every client holds Odoo database credentials, tracks a per-instance URL, and re-implements the same request envelope. ODXProxy is a reverse proxy that sits in front of one or more Odoo instances and collapses all of that into a single JSON-RPC 2.0 endpoint:

POST /api/odoo/execute

Your application talks to that one route; the proxy forwards each call to the Odoo instance named in the request body. There is no per-model endpoint here either — the same POST /api/odoo/execute serves your whole ERP, and the model and action ride in the JSON body.

That design introduces two secrets you must keep straight, because conflating them is the most common cause of a mysterious 401:

  • x-api-key — the proxy's key. It authenticates your app to the proxy and travels as an HTTP header. It is the same for every Odoo instance behind that proxy.
  • odoo_instance.api_key — the Odoo user's key. It authenticates the actual ERP call and is sent per request inside the body.

They are never the same value. Set up the credentials side of this in How to authenticate to the Odoo API.

The 9 actions that replace a REST route table

Because the endpoint never changes, the thing that names your operation is the action field. The proxy accepts exactly nine actions — anything else is rejected with HTTP 400 before it ever reaches Odoo:

ActionWhat it does
search_countCount records matching a domain.
searchReturn the ids matching a domain.
readRead fields for a list of ids.
fields_getDescribe a model's fields.
search_readSearch and read in one call.
createCreate one or more records.
writeUpdate records by id.
unlinkDelete records by id.
call_methodInvoke any other model method, named by fn_name.

The first eight are the CRUD-and-query core. The ninth, call_method, is the escape hatch: any model method your user can call — action_post on account.move, action_confirm on sale.order — you reach by setting action to call_method and fn_name to the method name. That single action is why the tiny endpoint list loses nothing in capability. See it in action in Calling Odoo model methods with call_method.

A worked request

Here is a complete round trip: a search_read that returns five companies. Notice that the model and the action are body fields, not part of the URL:

curl -X POST https://your-proxy.example.com/api/odoo/execute \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ODX_PROXY_KEY" \
  -d '{
    "id": "endpoints-demo-1",
    "action": "search_read",
    "model_id": "res.partner",
    "params": [[["is_company", "=", true]]],
    "keyword": { "fields": ["name", "email"], "limit": 5 },
    "odoo_instance": {
      "url": "https://erp.example.com",
      "db": "prod",
      "user_id": 2,
      "api_key": "<the Odoo user API key>"
    }
  }'

Every request is the same five-part shape:

  • action — one of the nine above.
  • model_id — the Odoo model, e.g. res.partner or sale.order.
  • params — a JSON array of positional arguments (default []). For search_read the first positional argument is the domain, which is why it is wrapped one level deep: [[["is_company", "=", true]]].
  • keyword — a JSON object of keyword arguments (default {}) — here fields and limit.
  • id — a string you choose; it is echoed back so you can correlate responses with requests.

On success you get HTTP 200 and a JSON-RPC envelope whose result holds Odoo's data:

{
  "jsonrpc": "2.0",
  "id": "endpoints-demo-1",
  "result": [
    { "id": 9, "name": "Gemini Furniture", "email": "info@gemini.example" },
    { "id": 14, "name": "Azure Interior", "email": "hello@azure.example" }
  ]
}

Want to change the operation? Change action and model_id. The endpoint stays exactly the same.

The endpoints that aren't the data API

A few more routes exist, and it is worth knowing they are not where your data calls go:

  • POST /api/odoo/version — returns the target Odoo instance's public version banner. It needs the instance URL but no Odoo credentials, which makes it a handy connectivity check.
  • GET /_/license, GET /_/about, GET /_/metrics — operational endpoints for license status, build info, and Prometheus metrics. None require the x-api-key. Treat these as ops tooling, separate from the data API — don't wire them into your integration's call path.

The HTTP 200 trap

There is one rule that trips up nearly every new integration: you cannot infer success from the HTTP status alone. There are two layers of failure, checked in order.

Proxy-level failures use a non-200 status. A wrong proxy key returns 401 (-32000); an action outside the allowlist returns 400 (-32001); an upstream timeout returns 504 (-32003).

Odoo's own errors come back with HTTP 200. If the proxy accepted the request but Odoo rejected it — a bad Odoo API key, a missing access right, a validation failure — Odoo's logic error is passed straight through with a 200 and a populated error object:

{
  "jsonrpc": "2.0",
  "id": "endpoints-demo-1",
  "error": {
    "code": 200,
    "message": "You are not allowed to create 'Contact' records.",
    "data": { "name": "odoo.exceptions.AccessError" }
  }
}
A 200 response is not proof of success. Check the HTTP status first, and then — even on a 200 — check whether the body has a populated error field before you read result.

The full two-layer model, with every error code and a robust client, is in Odoo API error handling.

Where to go next

The takeaway is worth internalizing: Odoo has almost no endpoints, and that is the point. One universal method, one route through the proxy, and nine actions cover the entire ERP.