Documentation Index
Fetch the complete documentation index at: https://mintlify.com/odoo/documentation/llms.txt
Use this file to discover all available pages before exploring further.
The Odoo External JSON-2 API (/json/2) is the modern, recommended way to integrate external software with an Odoo instance. It exposes the full ORM model API over HTTP with JSON payloads, requiring only an API key for authentication. The /json/2 endpoint was introduced in Odoo 19.0.
Access to the External API requires an Odoo Custom pricing plan. It is not available on One App Free or Standard plans. Visit the Odoo pricing page for details.
URL
POST /json/2/<model>/<method>
<model> — the technical model name, e.g. res.partner, sale.order
<method> — the ORM or business method to call, e.g. search_read, create, write
| Header | Required | Description |
|---|
Host | Yes (HTTP/1.1) | Hostname of the Odoo server |
Authorization | Yes | bearer <api_key> |
Content-Type | Yes | application/json (charset recommended) |
X-Odoo-Database | Optional | Database name (required when a single server hosts multiple databases) |
User-Agent | Recommended | Name of your client software |
Request Body (JSON)
| Key | Description |
|---|
ids | Array of record IDs. Omit or leave empty for @api.model methods. |
context | Optional object with extra context, e.g. {"lang": "en_US"} |
param | Any additional named arguments the method accepts |
Example Request
POST /json/2/res.partner/search_read HTTP/1.1
Host: mycompany.example.com
X-Odoo-Database: mycompany
Authorization: bearer 6578616d706c65206a736f6e20617069206b6579
Content-Type: application/json; charset=utf-8
User-Agent: mysoftware python-requests/2.25.1
{
"context": {"lang": "en_US"},
"domain": [
["name", "ilike", "%deco%"],
["is_company", "=", true]
],
"fields": ["name"]
}
Success — HTTP 200 with the JSON-serialized return value:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
[{"id": 25, "name": "Deco Addict"}]
Error — HTTP 4xx/5xx with a JSON error object:
| Field | Description |
|---|
name | Fully qualified Python exception class name |
message | Exception message |
arguments | All exception arguments |
context | Request context at time of error |
debug | Full Python traceback (for debugging) |
{
"name": "werkzeug.exceptions.Unauthorized",
"message": "Invalid apikey",
"arguments": ["Invalid apikey", 401],
"context": {},
"debug": "Traceback (most recent call last):\n..."
}
API Key Management
Manual Key Generation
Navigate to Preferences → Account Security → New API Key. Provide a description and duration. The key value is shown only once — copy it immediately.
API keys cannot be retrieved after creation. If you lose a key, delete it and generate a new one. For security, keys cannot be created with durations longer than three months.
Programmatic Key Generation
Keys can be generated via the API itself using res.users.apikeys/generate:
import requests
API_KEY = "..." # from secure storage
res = requests.post(
"https://mycompany.example.com/json/2/res.users.apikeys/generate",
headers={"Authorization": f"bearer {API_KEY}"},
json={
"key": API_KEY,
"scope": None,
"name": "My integration service",
"expiration_date": "2026-05-19",
},
)
res.raise_for_status()
new_api_key = res.json()
# store new_api_key securely
generate parameters:
| Parameter | Type | Description |
|---|
key | string | An existing valid API key used to authenticate |
scope | string | null | Scope restriction ("rpc" for RPC access, null for unscoped) |
name | string | Human-readable label |
expiration_date | string | ISO 8601 date, e.g. "2026-05-19" |
Key Revocation
API keys can be revoked with res.users.apikeys/revoke. Pass the key to be revoked in the request body; authenticate with a valid key in the Authorization header (the key being revoked and the one in the header do not need to match):
import requests
API_KEY = "..." # from secure storage
res = requests.post(
"https://mycompany.example.com/json/2/res.users.apikeys/revoke",
headers={"Authorization": f"bearer {API_KEY}"},
json={"key": API_KEY},
)
res.raise_for_status()
If the key is valid it is revoked immediately. If not, the request fails with HTTP 403 Forbidden.
Key rotation best practice: Generate the new key first. Store it securely and verify all services are using it. Then use the new key to revoke the previous one. Use separate API keys per service or integration to simplify auditing and revocation.
Transactions
Each /json/2 call runs in its own SQL transaction — committed on success, rolled back on error. It is not possible to chain multiple calls in a single transaction. To ensure atomicity across multiple ORM operations, call a single model method that performs all operations internally.
# ✅ Atomic — both search and read happen in one transaction
POST /json/2/res.partner/search_read
# ❌ Non-atomic — two separate transactions
POST /json/2/res.partner/search → ids
POST /json/2/res.partner/read → records (a concurrent write could have changed records)
Code Examples
The following examples search for partner companies containing “deco” in their name and read their names.
import requests
BASE_URL = "https://mycompany.example.com/json/2"
API_KEY = "..." # from secure storage
headers = {
"Authorization": f"bearer {API_KEY}",
"X-Odoo-Database": "mycompany",
}
# Search for matching partner IDs
res_search = requests.post(
f"{BASE_URL}/res.partner/search",
headers=headers,
json={
"context": {"lang": "en_US"},
"domain": [
("name", "ilike", "%deco%"),
("is_company", "=", True),
],
},
)
res_search.raise_for_status()
ids = res_search.json()
# Read the names of those partners
res_read = requests.post(
f"{BASE_URL}/res.partner/read",
headers=headers,
json={
"ids": ids,
"context": {"lang": "en_US"},
"fields": ["name"],
},
)
res_read.raise_for_status()
print(res_read.json())
Common ORM Methods
| Method | @api.model | Description |
|---|
search | ✅ | Returns IDs matching a domain filter |
search_read | ✅ | Search + read in one atomic call |
read | ❌ (needs ids) | Read specific fields of given record IDs |
create | ✅ | Create one or more records |
write | ❌ (needs ids) | Update fields on given record IDs |
unlink | ❌ (needs ids) | Delete records with given IDs |
fields_get | ✅ | Return field metadata for the model |
create
new_id = requests.post(
f"{BASE_URL}/res.partner/create",
headers=headers,
json={
"values": {"name": "New Company", "is_company": True},
},
).json()
write
requests.post(
f"{BASE_URL}/res.partner/write",
headers=headers,
json={
"ids": [new_id],
"values": {"name": "Updated Company Name"},
},
)
unlink
requests.post(
f"{BASE_URL}/res.partner/unlink",
headers=headers,
json={"ids": [new_id]},
)
Migrating from XML-RPC / JSON-RPC
The legacy RPC APIs at /xmlrpc, /xmlrpc/2, and /jsonrpc are scheduled for removal in Odoo 22 (fall 2028). The JSON-2 API replaces the object service with a cleaner design:
| Legacy (XML-RPC) | JSON-2 |
|---|
uid + password in every call | Bearer API key in Authorization header |
model, method as arguments | model, method in the URL path |
Positional args in execute() | Named args in the JSON body |
context only via execute_kw | context always available in the body |