ODXProxy

API Versions (v1 and v2)

ODXProxy speaks to Odoo two ways — execute_kw over /jsonrpc (v1) and Odoo's JSON-2 API (v2). Which to use, what changes, and how to migrate.

ODXProxy has two API versions. They share the same x-api-key auth, the same headers, and the same JSON-RPC 2.0 response envelope; they differ in how the proxy talks to Odoo, and therefore in the request shape.

v1v2
EndpointsPOST /api/odoo/execute, POST /api/odoo/version (alias /v1/odoo/*)POST /v2/odoo/execute, POST /v2/odoo/version
UpstreamOdoo's execute_kw over /jsonrpcOdoo's JSON-2 API, POST /json/2/<model>/<method>
Odoo versionsany, up to 2119 and later
ODXProxyany version0.9.0 or later
Requestaction (one of 9) + positional params + keywordmodel_id + any public method + one named kwargs object
Odoo credentialsurl, db, user_id, api_keyurl, db, api_key — no user_id

Which one to use

Your OdooUse
18 or olderv1 — Odoo has no JSON-2 yet.
19, 20, 21Either. Prefer v2 for new code that must survive an upgrade to 22; stay on v1 if the same code also has to reach older instances.
22 and laterv2 — Odoo 22 removes /jsonrpc, which v1 depends on.

v1 is not deprecated: it is the only way to reach Odoo 18 and older, and it stays for as long as customers run those versions.

If you don't know an instance's version, ask the proxy once and cache the answer per URL:

curl -s -X POST http://localhost:6600/v2/odoo/version \
  -H "x-api-key: $PROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "id": "1", "url": "https://erp.example.com" }'
{ "jsonrpc": "2.0", "id": "1", "result": { "version_info": [20, 0, 0, "final", 0, "e"], "version": "20.0+e" } }

version_info[0] of 19 or more means v2 is available. Error -32006, or a major version below 19, means use v1. Don't probe before every call — the answer only changes when the instance is upgraded. Every SDK has a cached helper for this.

The same call on both

A search_read on v1:

{
  "id": "1",
  "action": "search_read",
  "model_id": "res.partner",
  "params": [[["is_company", "=", true]]],
  "keyword": { "fields": ["name", "email"], "limit": 10 },
  "odoo_instance": { "url": "https://erp.example.com", "db": "prod", "user_id": 2, "api_key": "<ODOO_USER_API_KEY>" }
}

and on v2:

{
  "id": "1",
  "model_id": "res.partner",
  "method": "search_read",
  "kwargs": {
    "domain": [["is_company", "=", true]],
    "fields": ["name", "email"],
    "limit": 10
  },
  "odoo_instance": { "url": "https://erp.example.com", "db": "prod", "api_key": "<ODOO_USER_API_KEY>" }
}

Three differences show up immediately:

  • Named arguments only. v2 has no positional params and no keyword. Every argument goes in kwargs under the Odoo method's Python parameter name, so the domain is "domain" — the list itself, without v1's extra wrapping.
  • Any public method. method names the Odoo method directly; there's no action allowlist and no fn_name. Odoo still refuses private (_-prefixed) methods, and every call still runs under the Odoo user's own permissions.
  • No user_id. JSON-2 derives the user from the API key. A user_id sent by a v1-shaped client is ignored, so one instance object can serve both versions.

The response envelope is identical. See Actions and methods for the kwargs of every common method.

v2 in production: what to know

An API key, not a password — and it can expire

v2 accepts only an Odoo API key. On Odoo 20+ its scope must be rpc (the default in Odoo's key wizard). Keys of non-admin users expire on a schedule; an expired key comes back as HTTP 200 with error code 401 — Odoo's, not the proxy's -32000. Surface it to the end user so they can issue a new key.

Multi-database hosts: dbfilter applies

JSON-2 selects the database with a header, and Odoo's dbfilter filters it. On hosts that pick the database from the hostname (for example dbfilter = ^%d$, common on multi-tenant hosting), odoo_instance.url must be that database's own hostname, or every call fails with -32006. v1 isn't affected. A -32006 from a server you know runs 19+ is a database/hostname problem, not a version problem.

  • Company context is explicit. Odoo applies no company selection unless the call's context carries allowed_company_ids; company_dependent fields then read as the API user's default company. Send it in kwargs.context.
  • create always returns a list of ids — {"vals_list": [{...}]} → [42], even for one record. (On v1, creating from a single dict returns a bare id.)
  • Arguments are checked against the method signature. An unknown or misspelled key, or ids sent to an @api.model method such as search or create, fails with error code 422. Keys are never camelCased: vals_list, not valsList.
  • Errors carry Odoo's HTTP status. An Odoo-side error arrives on HTTP 200 with error.code set to 401, 403, 404, 409, 422, or 500, and error.data set to Odoo's error object. Only 409 (a lock conflict) is worth retrying. See Error codes.

Two Odoo 20 data changes apply to both versions, so they matter whichever you pick:

  • Binary fields read as {"content": "<base64>", "filename": "...", "size": n} rather than a bare base64 string.
  • Empty values are still false, not null, and datetimes are still UTC strings ("YYYY-MM-DD HH:MM:SS").

Migrating from v1 to v2

Most calls translate mechanically. ids and the values dict become named keys, and the domain loses one level of nesting:

v1 action + paramsv2 method + kwargs
search_read, params: [domain], keyword: {fields, limit, offset, order}search_read, {domain, fields, limit, offset, order}
search, params: [domain]search, {domain, limit, offset, order}
search_count, params: [domain]search_count, {domain}
read, params: [ids, fields]read, {ids, fields}
fields_get, keyword: {attributes}fields_get, {attributes} (and allfields)
create, params: [vals]create, {vals_list: [vals]} → returns [id]
write, params: [ids, vals]write, {ids, vals}
unlink, params: [ids]unlink, {ids}
call_method, fn_name: "action_post", params: [ids]action_post, {ids}
call_method, fn_name: "name_search", params: ["Acm"], keyword: {limit: 5}name_search, {name: "Acm", limit: 5}
keyword.contextkwargs.context

For a method outside this table, look up its Python parameter names — on Odoo 19+, /doc/<model> in the web UI lists them — and send each one by name. Pass ids only when the method works on records.

Try it

  • The Postman collection has a v2 folder that runs top to bottom: version, search_read, search_count, fields_get, create, read, write, both kinds of method call, and unlink (which deletes the record it created). Each request has saved example responses, including the typical errors.
  • Every SDK supports both versions, with v2 as a separate session or namespace, so moving a call to v2 is mostly a constructor change.
  • The full v2 endpoint schema is in the API reference.

On this page