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:
- Go to
Preferences > Account Security - Click New API Key
- Enter a description (use something that identifies where the key is used — not just “API key”)
- Set an expiration date — keep it short for interactive use
- 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.