← Back to Blog
Development by Featured

Odoo's New /json/2 API: What Changed and How to Migrate Before the Deadline

Odoo's New /json/2 API: What Changed and How to Migrate Before the Deadline

Odoo 19 introduced /json/2, a modern HTTP API that replaces /jsonrpc and /xmlrpc.

This post covers what the new /json/2 API is, how it differs from what came before, how to generate API keys, and how to rewrite the most common integration patterns.


Why the old API is going away

The XML-RPC and JSON-RPC endpoints have been Odoo’s external API since the early days. They work, but they carry years of design decisions that were reasonable at the time and are now friction.

Two problems stand out in practice.

The first is authentication. Both APIs authenticate with a username and password. For machine-to-machine integrations, that means storing a user’s credentials somewhere, rotating them when the user changes their password, and accepting that a compromised credential gives full user-level access. API keys are a standard solution to this problem, but the old API never adopted them.

The second is error signaling. Both the XML-RPC and JSON-RPC endpoints return HTTP 200 on errors. The error information is buried inside the response body. That breaks every standard HTTP monitoring tool, every retry library, and every developer’s intuition about what a 200 means. You have to parse every response to find out whether the call succeeded.

/json/2 fixes both.


How /json/2 works

The structure is straightforward: POST /json/2/<model>/<method>.

The model is the technical model name (res.partner, sale.order). The method is what you are calling (search, read, search_read, create, write, unlink, or any custom method exposed externally). The request body is a JSON object containing the method’s arguments by name.

Request headers

Header Required Value
Authorization Yes bearer <API_KEY>
Content-Type Yes application/json (charset recommended)
X-Odoo-Database When needed Database name — only required when the domain serves multiple databases
User-Agent Recommended Your software name

Request body

{
  "context": { "lang": "en_US" },
  "domain": [
    ["name", "ilike", "%deco%"],
    ["is_company", "=", true]
  ],
  "fields": ["name"]
}

Arguments are named. There is no positional argument mode — you must use the exact parameter names defined on the Odoo method (ids, domain, fields, vals, vals_list, etc.).

Response

Success returns HTTP 200 with the method’s return value as JSON.

Errors return a 4xx or 5xx status with a JSON error object:

{
  "name": "werkzeug.exceptions.Unauthorized",
  "message": "Invalid apikey",
  "arguments": ["Invalid apikey", 401],
  "context": {},
  "debug": "Traceback (most recent call last): ..."
}

This is a meaningful change. With proper status codes, your monitoring stack can tell the difference between a working integration and a failing one without inspecting every response body.

Transaction behavior

Each call to /json/2 runs in its own SQL transaction. Success commits it; errors discard it. You cannot chain multiple calls into a single transaction through the external API.


Getting an API key

API keys are per-user and time-limited. Regular users can generate keys with a maximum lifetime of 90 days. Administrators can create keys with no expiry.

To generate a key manually:

  1. Go to Preferences > Account Security
  2. Click New API Key
  3. Enter a description (use something that identifies where the key is used — not just “API key”)
  4. Set an expiration date — keep it short for interactive use
  5. Click Generate Key

The key is a 160-bit random value. It is shown exactly once. Copy it immediately and store it outside your codebase — a secrets manager, environment variable, or vault. If you lose it, you have to revoke it and generate a new one.

⚠️ Keys last a maximum of 90 days for standard users. This means integrations need a key rotation process. Either automate rotation programmatically (see below) or set a calendar reminder before each key expires.

Programmatic key generation

If you need to rotate keys without manual UI steps:

import requests

API_KEY = ...  # current key, from a secure location

res = requests.post(
    "https://mycompany.example.com/json/2/res.users.apikeys/generate",
    headers={"Authorization": f"bearer {API_KEY}"},
    json={
        "name": "my-integration-service",
        "expiration_date": "2026-09-22",
        "scope": None,
    },
)
res.raise_for_status()
new_key = res.json()  # store this securely, then retire the old key

A working integration in Python

import requests

BASE_URL = "https://mycompany.example.com/json/2"
API_KEY = ...  # from a secure location

headers = {
    "Authorization": f"bearer {API_KEY}",
    "X-Odoo-Database": "mycompany",
    "User-Agent": "my-integration " + requests.utils.default_user_agent(),
}

# search
res = requests.post(
    f"{BASE_URL}/res.partner/search",
    headers=headers,
    json={
        "context": {"lang": "en_US"},
        "domain": [["name", "ilike", "%deco%"], ["is_company", "=", True]],
    },
)
res.raise_for_status()
ids = res.json()

# read
res = requests.post(
    f"{BASE_URL}/res.partner/read",
    headers=headers,
    json={
        "ids": ids,
        "context": {"lang": "en_US"},
        "fields": ["name", "email"],
    },
)
res.raise_for_status()
records = res.json()

No XML parsing. No RPC envelope. No checking the response status code buried in a JSON payload. The HTTP layer carries the semantics.


What the migration actually looks like

Here is a side-by-side of the same operation — reading partners — in the old XML-RPC style versus /json/2.

Old (XML-RPC, Python xmlrpc.client):

import xmlrpc.client

url = "https://mycompany.example.com"
db = "mycompany"
username = "admin"
password = "admin_password"

common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})

models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
partners = models.execute_kw(
    db, uid, password,
    "res.partner", "search_read",
    [[["name", "ilike", "%deco%"]]],
    {"fields": ["name"], "limit": 10}
)

New (/json/2, requests):

import requests

partners = requests.post(
    "https://mycompany.example.com/json/2/res.partner/search_read",
    headers={"Authorization": "bearer <API_KEY>", "X-Odoo-Database": "mycompany"},
    json={"domain": [["name", "ilike", "%deco%"]], "fields": ["name"], "limit": 10},
).json()

The old version needs two round-trips (authenticate, then call). The new version needs one. The old version passes credentials on every call. The new version sends a key that never exposes the user’s password.

What to watch for when rewriting

Named arguments only. XML-RPC allowed positional arguments. /json/2 does not. If your call uses execute_kw with a positional list, you need to check the method signature and name the arguments explicitly. write takes vals, not a positional dict. create takes vals_list for bulk creation.

No db or common service equivalent. The old db and common RPC services (list databases, get server version, authenticate) are not part of /json/2. Version info moved to GET /web/version. User ID retrieval is done via res.users/context_get with no ids — the API extracts the user from the key automatically.

/xmlrpc/2/object only, not /xmlrpc/2/db. The /json/2 endpoint replaces the object service. Database management via RPC is not migrated — and that is intentional.


Key differences at a glance

XML-RPC / JSON-RPC /json/2
Authentication Username + password Bearer API key
Key lifetime As long as the user exists 90 days max (regular user)
Error signaling Always HTTP 200, check body HTTP 4xx / 5xx
Argument style Positional or named Named only
URL structure /xmlrpc/2/object fixed /json/2/<model>/<method>
Transaction scope Varies One transaction per call
Big integer support Limited (XML-RPC) Full
Dynamic docs None /doc per database
Removal Odoo 22 / Online 21.1 Current and maintained

Deprecation timeline

Platform Deprecated Removed
Odoo Online (SaaS) Odoo 19 Online 21.1 (winter 2027)
Odoo.sh Odoo 19 Odoo 22 (fall 2028)
On-premise Odoo 19 Odoo 22 (fall 2028)

If you are on Odoo Online, that is roughly 18 months. On Odoo.sh or on-prem, you have until fall 2028. In both cases, the sensible time to migrate is before your next major upgrade, not the week before the deadline.

⚠️ Note: The deprecation affects /xmlrpc, /xmlrpc/2, and /jsonrpc. It does not affect the internal @route(type='jsonrpc') controllers used by Odoo’s own web client — those are a separate system and are not being removed.


Exploring the API on your own instance

Every Odoo 19 instance exposes dynamic documentation at /doc. This shows you the exact models, methods, fields, and access groups available in your specific database, with runnable examples. If you are building an integration and are not sure which method to call or what arguments it takes, that is the place to start.

Ready to get the most out of Odoo?

Whether you are starting a new implementation, upgrading from an older version, or optimizing your current setup — our Odoo-first team is here to help.