JSON-RPC is Odoo’s standard way for outside code to read and write your data over plain HTTP, and it works the same in Odoo 17, 18, and 19. If you can send a JSON POST request, you can log in, search records, create them, and update them, no XML, no special client library, and no add-on module required.
This guide covers the two endpoints you need, five worked examples in Python, the mistakes that catch almost every first integration, and where JSON-RPC stops being the right tool. We use this protocol ourselves to move data in and out of client Odoo systems, so the examples below are the same shape as what runs in production, not a stripped-down demo.
What JSON-RPC is, and when to reach for it
JSON-RPC is a small protocol for calling a function on a remote server and getting the result back, wrapped in JSON. There is no resource-based URL structure to design and no HTTP verb to pick correctly. You send a POST request naming a method and its arguments, and the server replies with a result or an error, in the same envelope every time.
Odoo exposes this at the same layer its own web interface uses internally: model methods such as search_read, create, write, and unlink, called through one generic dispatcher. You are not learning a bolted-on integration API. You are calling the exact methods Odoo’s own screens call when a user clicks a button.
Odoo also ships XML-RPC, an older sibling that does the same job in XML instead of JSON. Both are fully supported in Odoo 17, 18, and 19, and both expose the same underlying methods. JSON-RPC is generally the more pleasant of the two to write and debug, since most languages parse JSON natively and the payloads are shorter. If you already have working XML-RPC code, there is no need to rewrite it. The two are equivalent in what they can do.
There is a third, newer path worth knowing about before you start: MCP (Model Context Protocol), built for connecting AI assistants such as Claude or ChatGPT to Odoo. MCP is not a competing way to write custom integration code. It is a layer on top that lets an assistant call Odoo safely without you hand-writing the plumbing underneath. If a developer, a script, or your own application needs to talk to Odoo, JSON-RPC is still the right layer. If you want an AI assistant reading and writing Odoo on someone’s behalf, skip to the MCP section near the end.
The two endpoints you need
Odoo’s JSON-RPC surface looks bigger than it is. In practice, almost everything you do runs through two URLs on your Odoo domain.
/web/session/authenticate logs a user in. Post your database name, login, and password, or an API key, here once, and Odoo responds with your user id and sets a session cookie. You call this endpoint once per session.
/web/dataset/call_kw does the work. Every search, read, create, write, or custom model method goes through this one endpoint, with the target model, the method name, and its arguments in the request body. Once you are authenticated, this is the only URL you post to for the rest of the script.
There is also a lower-level endpoint, /jsonrpc, which mirrors Odoo’s older XML-RPC pattern directly: you pass a service name (common, db, or object), a method (authenticate, execute_kw), and the database, user id, and password on every call, instead of relying on a cookie. It suits one-off scripts or environments where holding a cookie is awkward. For anything that makes more than a call or two, the session-based pair above is less code, and it is exactly what Odoo’s own browser interface uses.
Step by step, with real code
These examples use Python’s requests library against a live Odoo database. The pattern is identical in JavaScript, PHP, or anything else that can send a JSON POST and hold a cookie. Only the syntax changes.
1. Authenticate and get a session
Start a requests.Session() so the cookie Odoo sets is carried automatically on every later call.
import requests
url = "https://your-instance.odoo.com"
session = requests.Session()
payload = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"db": "your_database",
"login": "integration@yourcompany.com",
"password": "your_api_key_or_password",
},
"id": 1,
}
response = session.post(f"{url}/web/session/authenticate", json=payload)
result = response.json()
if result.get("error"):
raise Exception(result["error"])
uid = result["result"]["uid"]
print(f"Authenticated as user {uid}")
2. Read records with search_read
Everything else goes through call_kw, so it is worth wrapping in one small helper before the next three steps.
def call_kw(session, url, model, method, args=None, kwargs=None, request_id=2):
payload = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"model": model,
"method": method,
"args": args or [],
"kwargs": kwargs or {},
},
"id": request_id,
}
response = session.post(f"{url}/web/dataset/call_kw", json=payload)
data = response.json()
if "error" in data:
raise Exception(data["error"])
return data["result"]
customers = call_kw(
session, url,
model="res.partner",
method="search_read",
args=[[["customer_rank", ">", 0]], ["id", "name", "email", "phone"]],
kwargs={"limit": 20},
)
for c in customers:
print(c["id"], c["name"], c["email"])
3. Create a record
new_id = call_kw(
session, url,
model="res.partner",
method="create",
args=[{
"name": "Acme Industrial Supply",
"email": "ap@acme-industrial.example",
"phone": "+1 555 010 1234",
}],
)
print(f"Created partner {new_id}")
4. Update a record with write
call_kw(
session, url,
model="res.partner",
method="write",
args=[[new_id], {"phone": "+1 555 010 9999"}],
)
write takes a list of ids as its first argument, even for a single record, and a dict of the fields to change as its second. It returns true on success, not the changed record, so read the record back if you need to see the result.
5. Handle errors and the session cookie
A failed call does not usually raise an HTTP error. Odoo answers with HTTP 200 and puts the problem inside the JSON body instead:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": 200,
"message": "Odoo Server Error",
"data": {
"name": "odoo.exceptions.AccessError",
"message": "You are not allowed to access 'Contact' (res.partner) records.",
"arguments": ["You are not allowed to access 'Contact' (res.partner) records."]
}
}
}
Always check for an “error” key before trusting “result”. data.name gives you the exception class, AccessError, ValidationError, and UserError are the common ones, and data.message gives the human-readable reason. If calls start failing partway through a long-running script with a session or authentication error, the cookie has expired. Catch it and call /web/session/authenticate again rather than letting the whole job crash.
Common gotchas
Most first integrations hit the same handful of walls, and none of them are exotic.
- Losing the session cookie. If you authenticate and then send later calls without carrying that cookie forward, each one lands as an anonymous request and fails. Reuse one requests.Session() object for the whole script, or keep a proper cookie jar if you are working from curl.
- Missing access rights. A clean, well-formed request that comes back with an AccessError almost never means the JSON is wrong. It means the user you authenticated as lacks read, write, create, or unlink rights on that model, or a record rule is filtering the row out before you ever see it. Check the user’s access rights before you check your code.
- Confusing the request id with the record id. The top-level “id” field in the JSON-RPC envelope is just a number you choose to match a response to its request, and Odoo echoes it back unchanged. The record’s own id comes back separately, inside “result”.
- Forgetting domains are lists of lists. A domain filter is always a list of conditions, even when there is only one: [[“state”, “=”, “done”]], not [“state”, “=”, “done”]. To combine more than one condition with anything other than a plain AND, add the prefix operator first: ‘|’ for or, ‘&’ for and, ‘!’ for not.
When to bring in a partner
A short script that reads a handful of records is a reasonable weekend project. A production integration that keeps a website, a warehouse system, or a partner’s ERP in sync with Odoo is a different job, and this is usually where a DIY build starts to strain.
Odoo does not publish a hard rate limit, but a database under sustained integration traffic competes with your own users for worker processes and connections. Batching calls, backing off on failure, and watching what your integration is doing to the server all matter once it runs daily rather than once in testing. Production error handling means more than a try/except: retries need to be safe to repeat without creating duplicate records, and a session that expires overnight needs to recover on its own. Security means running the integration as its own named user with only the access rights that job requires, authenticating with an API key instead of a shared password, and keeping that key out of source control.
We are an official Odoo partner with an in-house development team, and we build integrations like this in production, not only for tutorials. If you want this built and maintained rather than assembled from a blog post, that is work our developers do. If the integration is the whole project rather than one script, our integration services cover the design, the error handling, and the support around it afterward.
The modern evolution: MCP instead of hand-rolled RPC
Everything above is for code you write and maintain yourself: a script, a sync job, an app. If what you want is an AI assistant such as Claude or ChatGPT reading and writing Odoo directly, in plain language, MCP is the current way to do that instead of hand-rolling JSON-RPC calls behind a chatbot. It wraps the same call_kw machinery this guide covers with permission and audit controls built for an assistant rather than a developer, so you are not left exposing your whole database to whatever the model decides to ask for. Our AI MCP Connector does exactly this for Odoo 17, 18, and 19.
Frequently asked questions
What is JSON-RPC in Odoo?
JSON-RPC is Odoo’s standard protocol for reading and writing data from outside code. You send a JSON request over HTTP to an Odoo endpoint and get a JSON response back, the same mechanism Odoo’s own web client uses to talk to its server.
Does JSON-RPC work in Odoo 18 and 19?
Yes. The JSON-RPC endpoints and the call_kw request format are unchanged across Odoo 17, 18, and 19. Code written against Odoo 17 keeps working on 18 and 19 without modification, provided the models and fields it calls still exist in your database.
What is the difference between JSON-RPC and XML-RPC in Odoo?
Both expose the same underlying methods, authenticate, execute_kw, search_read, create, and write, and both are fully supported in current Odoo versions. JSON-RPC sends JSON instead of XML, which is lighter to send and easier to read in logs. Pick whichever your language’s tooling makes easier; Odoo behaves the same either way.
How do I authenticate with the Odoo JSON-RPC API?
Post your database name, login, and password or API key to /web/session/authenticate. Odoo returns your user id and sets a session cookie, and a requests.Session() in Python carries that cookie automatically on every later call to /web/dataset/call_kw.
Is JSON-RPC secure for production?
It is exactly as secure as the credentials and access rights behind it. Use a dedicated integration user with only the access rights it needs, authenticate with an API key rather than a real password, and run everything over HTTPS. JSON-RPC has no security layer of its own; it inherits Odoo’s normal user permissions and record rules.
Should I use JSON-RPC or MCP for connecting AI to Odoo?
For a script or an application you control, JSON-RPC is the direct, simpler choice. For an AI assistant such as Claude or ChatGPT, use MCP instead. It wraps this same call_kw machinery with permission and audit controls built for an assistant rather than a developer writing code by hand.
Bring your JSON-RPC question, or the integration you are stuck on, to a free demo and we will look at it with you.


