Skip to main content

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.

Request Format

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

HTTP Headers

HeaderRequiredDescription
HostYes (HTTP/1.1)Hostname of the Odoo server
AuthorizationYesbearer <api_key>
Content-TypeYesapplication/json (charset recommended)
X-Odoo-DatabaseOptionalDatabase name (required when a single server hosts multiple databases)
User-AgentRecommendedName of your client software

Request Body (JSON)

KeyDescription
idsArray of record IDs. Omit or leave empty for @api.model methods.
contextOptional object with extra context, e.g. {"lang": "en_US"}
paramAny 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"]
}

Response Format

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:
FieldDescription
nameFully qualified Python exception class name
messageException message
argumentsAll exception arguments
contextRequest context at time of error
debugFull 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:
ParameterTypeDescription
keystringAn existing valid API key used to authenticate
scopestring | nullScope restriction ("rpc" for RPC access, null for unscoped)
namestringHuman-readable label
expiration_datestringISO 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.modelDescription
searchReturns IDs matching a domain filter
search_readSearch + read in one atomic call
read❌ (needs ids)Read specific fields of given record IDs
createCreate one or more records
write❌ (needs ids)Update fields on given record IDs
unlink❌ (needs ids)Delete records with given IDs
fields_getReturn 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"},
    },
)
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 callBearer API key in Authorization header
model, method as argumentsmodel, method in the URL path
Positional args in execute()Named args in the JSON body
context only via execute_kwcontext always available in the body

Build docs developers (and LLMs) love