Skip to main content
Technical GuideFebruary 21, 202618 min read

Odoo API Integration Guide — XML-RPC, JSON-RPC & REST (2026)

A practical reference for connecting external systems to Odoo. Includes working code in Python, JavaScript, and curl covering authentication, CRUD operations, search filters, file handling, and real-time updates.

Odoo exposes its entire data model through an external API. Every record you can see in the web interface (partners, sale orders, invoices, products, stock moves) is accessible programmatically. This is how ERPs connect with e-commerce platforms, payment gateways, shipping providers, BI tools, and custom mobile apps.

This guide covers all three API protocols Odoo supports, with working code you can copy and run. We will start with the protocols, move through authentication and CRUD, and finish with advanced patterns like file uploads, webhooks, and pagination. All examples work with Odoo 16, 17, and 18.

1. Odoo API Overview

Odoo provides three protocols for external API access. Each exposes the same underlying ORM methods, so the capabilities are identical. The difference is in the transport format and how you call them.

ProtocolFormatAvailable SinceBest For
XML-RPCXML over HTTP POSTOdoo 6+Legacy integrations, Python scripts
JSON-RPCJSON over HTTP POSTOdoo 8+JavaScript apps, modern integrations
REST (OWL)JSON over HTTP verbsOdoo 17+Standard REST clients, mobile apps

XML-RPC has the most documentation of the three. Python's standard library includes xmlrpc.client, so you do not need any third-party packages. Every Odoo tutorial from the past decade uses XML-RPC. The downside is that XML payloads are verbose, and parsing XML in JavaScript is painful.

JSON-RPC is what the Odoo web client uses under the hood. It sends JSON payloads to /jsonrpc , the same endpoint the browser hits. If you are building a JavaScript or TypeScript integration, JSON-RPC is the natural choice. The request/response format is simpler and payloads are 40-60% smaller than XML-RPC.

REST API (Odoo 17+) is the newest addition. It uses standard HTTP methods (GET to read, POST to create, PUT to update, DELETE to remove). This follows conventional REST patterns, making it easier for teams familiar with REST APIs from other platforms. As of Odoo 18 it remains experimental with limited documentation, but it works for straightforward CRUD.

Our recommendation: Use JSON-RPC for new integrations. It works in every language, the payloads are clean, and it covers 100% of the ORM. Use XML-RPC only if you are maintaining an existing integration or need Python's stdlib-only approach. Use REST if you are on Odoo 17+ and want standard HTTP semantics.

2. Authentication

Before making any data call, you need to authenticate. Odoo supports three methods depending on your use case and Odoo version.

2a. Database Authentication (XML-RPC)

The classic approach. Call xmlrpc/2/common to authenticate and get a user ID (uid), then use that uid with your password for every subsequent call to xmlrpc/2/object.

Python

import xmlrpc.client

url = "https://your-odoo.com"
db = "your-database"
username = "admin"
password = "admin"

# Authenticate
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})
print(f"Authenticated as uid: {uid}")

# Create object proxy for data operations
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")

curl

# Authenticate via XML-RPC
curl -X POST https://your-odoo.com/xmlrpc/2/common \
  -H "Content-Type: text/xml" \
  -d '<?xml version="1.0"?>
<methodCall>
  <methodName>authenticate</methodName>
  <params>
    <param><value><string>your-database</string></value></param>
    <param><value><string>admin</string></value></param>
    <param><value><string>admin</string></value></param>
    <param><value><struct></struct></value></param>
  </params>
</methodCall>'

2b. Session Authentication (JSON-RPC)

POST to /web/session/authenticate to get a session cookie. Use that cookie for all subsequent requests. This is how the Odoo web client authenticates.

JavaScript (fetch)

const url = "https://your-odoo.com";

// Authenticate — get session cookie
const authResponse = await fetch(`${url}/web/session/authenticate`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      db: "your-database",
      login: "admin",
      password: "admin",
    },
  }),
});

const authData = await authResponse.json();
const sessionId = authResponse.headers.get("set-cookie");
console.log("User ID:", authData.result.uid);

curl

# Authenticate via JSON-RPC — save session cookie
curl -X POST https://your-odoo.com/web/session/authenticate \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
      "db": "your-database",
      "login": "admin",
      "password": "admin"
    }
  }'

2c. API Key Authentication (Odoo 14+)

API keys are the recommended method for automated integrations. Generate one in Settings > Users > Preferences > API Keys. Then use the key as the password in XML-RPC/JSON-RPC calls, or pass it via the X-Api-Key header for REST endpoints.

Python (API key as password)

import xmlrpc.client

url = "https://your-odoo.com"
db = "your-database"
username = "admin"
api_key = "your-api-key-here"  # NOT the user password

# Authenticate with API key (use it as the password)
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, api_key, {})

# Use uid + API key for all data calls
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
partners = models.execute_kw(
    db, uid, api_key,
    'res.partner', 'search_read',
    [[['is_company', '=', True]]],
    {'fields': ['name', 'email'], 'limit': 5}
)
print(partners)

Security tip: API keys have the same permissions as the user who created them. Create a dedicated integration user with minimal access rights rather than using the admin account. Store keys in environment variables, never in source code.

3. CRUD Operations with Code Examples

All data operations go through execute_kw. This single method handles create, read, update, and delete across every model. The pattern is always the same:

execute_kw(database, uid, password, model, method, args, kwargs)

Create

Returns the ID of the new record.

Python (XML-RPC)

# Create a new partner
partner_id = models.execute_kw(
    db, uid, password,
    'res.partner', 'create',
    [{'name': 'Acme Corp', 'email': 'info@acme.com', 'is_company': True}]
)
print(f"Created partner ID: {partner_id}")  # e.g., 42

JavaScript (JSON-RPC)

const createPartner = await fetch(`${url}/jsonrpc`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      service: "object",
      method: "execute_kw",
      args: [
        db, uid, password,
        "res.partner", "create",
        [{ name: "Acme Corp", email: "info@acme.com", is_company: true }],
      ],
    },
  }),
});

const { result: partnerId } = await createPartner.json();
console.log("Created partner ID:", partnerId);

curl (JSON-RPC)

curl -X POST https://your-odoo.com/jsonrpc \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
      "service": "object",
      "method": "execute_kw",
      "args": [
        "your-database", 2, "admin",
        "res.partner", "create",
        [{"name": "Acme Corp", "email": "info@acme.com", "is_company": true}]
      ]
    }
  }'

Read

Pass a list of record IDs and specify which fields you want. Always specify fields explicitly — omitting the fields parameter returns all fields, which is slow on models with many computed fields.

Python

# Read specific fields from partner ID 42
partner = models.execute_kw(
    db, uid, password,
    'res.partner', 'read',
    [[42]],
    {'fields': ['name', 'email', 'phone', 'country_id']}
)
print(partner)
# [{'id': 42, 'name': 'Acme Corp', 'email': 'info@acme.com',
#   'phone': False, 'country_id': [233, 'United States']}]

JavaScript

const readPartner = await fetch(`${url}/jsonrpc`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      service: "object",
      method: "execute_kw",
      args: [
        db, uid, password,
        "res.partner", "read",
        [[42]],
        { fields: ["name", "email", "phone", "country_id"] },
      ],
    },
  }),
});

const { result: partners } = await readPartner.json();
console.log(partners);

Update

The write method takes a list of IDs and a dictionary of values to update. Returns True on success. You can update multiple records at once by passing multiple IDs.

Python

# Update partner 42
result = models.execute_kw(
    db, uid, password,
    'res.partner', 'write',
    [[42], {'name': 'Acme Corporation', 'phone': '+1-555-0100'}]
)
print(result)  # True

# Bulk update — set country for multiple partners
models.execute_kw(
    db, uid, password,
    'res.partner', 'write',
    [[42, 43, 44], {'country_id': 233}]  # 233 = United States
)

JavaScript

const updatePartner = await fetch(`${url}/jsonrpc`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      service: "object",
      method: "execute_kw",
      args: [
        db, uid, password,
        "res.partner", "write",
        [[42], { name: "Acme Corporation", phone: "+1-555-0100" }],
      ],
    },
  }),
});

Delete

The unlink method permanently deletes records. There is no undo, so always test with a staging database first. Some records (like posted invoices) cannot be deleted due to business rules; Odoo will raise a UserError if you try.

Python

# Delete partner 42
result = models.execute_kw(
    db, uid, password,
    'res.partner', 'unlink',
    [[42]]
)
print(result)  # True

curl

curl -X POST https://your-odoo.com/jsonrpc \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
      "service": "object",
      "method": "execute_kw",
      "args": [
        "your-database", 2, "admin",
        "res.partner", "unlink",
        [[42]]
      ]
    }
  }'

4. Searching and Filtering Records

Odoo uses domain filters — lists of tuples that define search criteria. Each tuple has three elements: (field, operator, value). Multiple tuples are AND-ed by default. Use '|' for OR logic.

Three search methods are available:

  • search — returns a list of record IDs matching the domain
  • search_read — returns records with field data (combines search + read in one call)
  • search_count — returns just the count of matching records

Python — search_read with domain filters

# Find all companies that are customers
customers = models.execute_kw(
    db, uid, password,
    'res.partner', 'search_read',
    [[
        ('is_company', '=', True),
        ('customer_rank', '>', 0),
    ]],
    {
        'fields': ['name', 'email', 'country_id', 'customer_rank'],
        'limit': 20,
        'offset': 0,
        'order': 'customer_rank desc',
    }
)

# Count total matching records (for pagination)
total = models.execute_kw(
    db, uid, password,
    'res.partner', 'search_count',
    [[
        ('is_company', '=', True),
        ('customer_rank', '>', 0),
    ]]
)
print(f"Showing {len(customers)} of {total} customers")

JavaScript — search_read with OR logic

// Find partners in US OR Canada
const response = await fetch(`${url}/jsonrpc`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      service: "object",
      method: "execute_kw",
      args: [
        db, uid, password,
        "res.partner", "search_read",
        [[
          "|",
          ["country_id.code", "=", "US"],
          ["country_id.code", "=", "CA"],
        ]],
        {
          fields: ["name", "email", "country_id"],
          limit: 50,
          order: "name asc",
        },
      ],
    },
  }),
});

Common domain operators:

OperatorDescriptionExample
=Equals('state', '=', 'sale')
!=Not equals('state', '!=', 'cancel')
>, <, >=, <=Comparison('amount_total', '>', 1000)
inIn list('state', 'in', ['sale', 'done'])
likePattern match (case-sensitive)('name', 'like', 'Acme%')
ilikePattern match (case-insensitive)('email', 'ilike', '%@acme.com')
child_ofHierarchical (parent/child)('category_id', 'child_of', 1)

5. Working with Relations

Relational fields in Odoo behave differently depending on their type. You need to understand this to build integrations that create or modify records with related data.

Many2one Fields

When you read a Many2one field, Odoo returns a tuple: [id, display_name]. When you write, just pass the integer ID.

# Reading a Many2one — returns [id, name]
partner = models.execute_kw(
    db, uid, password,
    'res.partner', 'read', [[42]],
    {'fields': ['name', 'country_id']}
)
# Result: [{'id': 42, 'name': 'Acme', 'country_id': [233, 'United States']}]

# Writing a Many2one — just pass the ID
models.execute_kw(
    db, uid, password,
    'res.partner', 'write',
    [[42], {'country_id': 38}]  # 38 = Canada
)

One2many and Many2many Fields

When you read a One2many or Many2many field, Odoo returns a list of IDs. Writing uses Odoo's special command syntax — a list of tuples where the first element is a command code:

CommandSyntaxDescription
0(0, 0, {values})Create a new record and link it
1(1, id, {values})Update an existing linked record
2(2, id, 0)Remove link and delete the record
3(3, id, 0)Remove link (do not delete the record)
4(4, id, 0)Link an existing record
5(5, 0, 0)Remove all links (do not delete records)
6(6, 0, [ids])Replace all links with the given list

Python — Creating a sale order with order lines

# Create a sale order with two order lines in one call
order_id = models.execute_kw(
    db, uid, password,
    'sale.order', 'create',
    [{
        'partner_id': 42,
        'order_line': [
            (0, 0, {
                'product_id': 1,
                'product_uom_qty': 5,
                'price_unit': 100.00,
            }),
            (0, 0, {
                'product_id': 2,
                'product_uom_qty': 10,
                'price_unit': 25.50,
            }),
        ],
    }]
)
print(f"Created sale order: {order_id}")

# Add another line to an existing order
models.execute_kw(
    db, uid, password,
    'sale.order', 'write',
    [[order_id], {
        'order_line': [
            (0, 0, {
                'product_id': 3,
                'product_uom_qty': 1,
                'price_unit': 500.00,
            }),
        ],
    }]
)

# Replace all tag IDs on a partner (Many2many)
models.execute_kw(
    db, uid, password,
    'res.partner', 'write',
    [[42], {
        'category_id': [(6, 0, [1, 5, 8])]  # Set tags to IDs 1, 5, 8
    }]
)

6. File Upload/Download via API

Odoo stores binary data (files, images, PDFs) as base64-encoded strings. When you read a binary field, you get the base64 data. When you write, you send base64. This applies to both the ir.attachment model and binary fields on other models (like image_1920 on products).

Python — Upload a file as an attachment

import base64

# Read the file and encode to base64
with open("invoice.pdf", "rb") as f:
    file_data = base64.b64encode(f.read()).decode("utf-8")

# Create an attachment linked to a partner
attachment_id = models.execute_kw(
    db, uid, password,
    'ir.attachment', 'create',
    [{
        'name': 'invoice.pdf',
        'type': 'binary',
        'datas': file_data,
        'res_model': 'res.partner',
        'res_id': 42,
        'mimetype': 'application/pdf',
    }]
)
print(f"Uploaded attachment ID: {attachment_id}")

Python — Download a file

# Read the attachment data
attachment = models.execute_kw(
    db, uid, password,
    'ir.attachment', 'read',
    [[attachment_id]],
    {'fields': ['name', 'datas', 'mimetype']}
)

# Decode and save
file_content = base64.b64decode(attachment[0]['datas'])
with open(attachment[0]['name'], "wb") as f:
    f.write(file_content)
print(f"Downloaded: {attachment[0]['name']}")

Python — Set a product image

# Upload a product image (automatically resized by Odoo)
with open("product-photo.jpg", "rb") as f:
    image_data = base64.b64encode(f.read()).decode("utf-8")

models.execute_kw(
    db, uid, password,
    'product.template', 'write',
    [[product_id], {'image_1920': image_data}]
)

Performance note: Base64 encoding adds ~33% overhead to file size. A 10 MB PDF becomes a 13.3 MB base64 string in the API payload. For bulk file imports, consider using Odoo's web controllers or direct filestore access instead of the RPC API to avoid memory pressure.

7. Webhooks and Real-Time Updates

Odoo does not have built-in webhook support like Stripe or GitHub. But there are reliable ways to trigger external calls when data changes.

7a. Automated Actions (Server Actions)

The most widely used approach. Go to Settings > Technical > Automated Actions and create a rule that triggers on record creation or update. Set the action type to "Execute Python Code" and use the requests library to POST to your webhook URL.

# Automated Action — Execute Python Code
# Trigger: On creation of sale.order
import json
import requests

for order in records:
    payload = {
        "event": "sale_order_created",
        "order_id": order.id,
        "partner": order.partner_id.name,
        "amount": order.amount_total,
        "currency": order.currency_id.name,
    }

    try:
        requests.post(
            "https://your-app.com/webhooks/odoo",
            json=payload,
            timeout=5,
            headers={"X-Webhook-Secret": "your-secret-key"},
        )
    except Exception:
        pass  # Log silently — do not block the order creation

7b. Scheduled Actions (ir.cron)

For polling-based integrations, use ir.cron to run periodic exports. This is useful when your external system cannot receive webhooks or when you need batch processing. Create a cron job that runs every N minutes and queries for records modified since the last run.

# Python — Poll for recently modified orders
from datetime import datetime, timedelta

# Get orders modified in the last 5 minutes
since = (datetime.utcnow() - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")

recent_orders = models.execute_kw(
    db, uid, password,
    'sale.order', 'search_read',
    [[('write_date', '>', since)]],
    {
        'fields': ['name', 'partner_id', 'amount_total', 'state'],
        'order': 'write_date desc',
    }
)

for order in recent_orders:
    print(f"Modified: {order['name']} — {order['state']} — {order['amount_total']}")

7c. Bus / Longpolling (Real-Time)

Odoo's bus module provides real-time notifications via longpolling. This is the same mechanism behind live chat and real-time notifications in the web client. It needs a session cookie and listens on /longpolling/poll. This is primarily useful for building web dashboards or chat integrations, not for typical API integrations.

# JavaScript — Subscribe to bus notifications
const pollResponse = await fetch(`${url}/longpolling/poll`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Cookie: sessionCookie,
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    method: "call",
    params: {
      channels: ["res.partner/notification"],
      last: 0,
    },
  }),
});

const notifications = await pollResponse.json();
console.log("Received:", notifications.result);

8. Rate Limiting and Best Practices

Odoo does not enforce rate limits by default, but your server has finite resources. A poorly written integration can bring down a production Odoo instance. These guidelines will help you avoid that.

Use search_read instead of search + read

search_read does both in a single round trip. Calling search to get IDs and then read to get data doubles your API calls and latency. The only exception is when you need the total count (use search_count separately) and the data (use search_read).

Always specify fields explicitly

Omitting the fields parameter returns every field on the model, including computed fields that trigger database queries. On sale.order, this can mean 80+ fields and multiple subqueries per record. Specifying the 5-10 fields you actually need can be 10x faster.

Paginate with limit and offset

Never fetch all records at once. Use limit=80 (or whatever your page size is) and offset for pagination. Always include an order parameter for consistent results across pages.

Batch write operations

Instead of calling write 100 times for 100 records, pass all 100 IDs to a single write call. Odoo processes this as one database transaction instead of 100.

Handle errors gracefully

Odoo returns XML-RPC faults or JSON-RPC error objects when something goes wrong. Always wrap API calls in try/except (Python) or try/catch (JavaScript). Common errors: AccessError (permission denied), ValidationError (invalid data), MissingError (record deleted).

Use connection pooling for high-throughput integrations

XML-RPC creates a new HTTP connection per call by default. For integrations that make hundreds of calls per minute, use requests.Session() (Python) or an HTTP agent with keep-alive (Node.js) to reuse TCP connections.

Test against a staging database

Never develop API integrations against production. Use a copy of your production database for testing. Odoo's duplicate database feature in the database manager makes this straightforward.

Python — Error handling pattern

import xmlrpc.client

try:
    result = models.execute_kw(
        db, uid, password,
        'res.partner', 'write',
        [[99999], {'name': 'Does not exist'}]
    )
except xmlrpc.client.Fault as e:
    # Odoo returns faults as XML-RPC Fault objects
    print(f"Odoo error: {e.faultString}")
    # Common faults:
    # - "Record does not exist or has been deleted"
    # - "Access Denied"
    # - "ValidationError"
except ConnectionError:
    print("Cannot reach the Odoo server")

Python — Batch operations with pagination

# Export all active products in batches of 100
batch_size = 100
offset = 0
all_products = []

while True:
    batch = models.execute_kw(
        db, uid, password,
        'product.template', 'search_read',
        [[('active', '=', True)]],
        {
            'fields': ['name', 'list_price', 'default_code', 'categ_id'],
            'limit': batch_size,
            'offset': offset,
            'order': 'id asc',
        }
    )

    if not batch:
        break

    all_products.extend(batch)
    offset += batch_size
    print(f"Fetched {len(all_products)} products so far...")

print(f"Total products exported: {len(all_products)}")

9. OEC.sh API

Outside of the Odoo API itself, OEC.sh has its own management API for automating server provisioning, deployments, and backups across your cloud infrastructure. If you are using OEC.sh to deploy and manage your Odoo instances, you can programmatically:

  • Provision new Odoo servers on any connected cloud provider
  • Trigger backups and restores
  • Scale server resources up or down
  • Monitor server health and performance metrics
  • Manage SSL certificates and domains

This is useful for agencies managing multiple client instances, or for CI/CD pipelines that need to deploy staging environments automatically. See pricing plans for API access details, or use the server calculator to estimate your infrastructure requirements.

Building an integration? You need a stable Odoo instance to test against. OEC.sh lets you spin up isolated staging environments in under 5 minutes on your own cloud account — deploy, test your API calls, and tear it down when you are done. No shared infrastructure, no noisy neighbors affecting your benchmarks.

Frequently Asked Questions

Does Odoo have a REST API?

Odoo 17+ introduced an experimental OWL-based REST API that accepts and returns JSON over standard HTTP methods (GET, POST, PUT, DELETE). For Odoo 16 and earlier, the external API uses XML-RPC and JSON-RPC protocols. Many teams also build custom REST endpoints using Odoo controller routes. Third-party modules like OCA’s REST framework add full REST/OpenAPI support to any Odoo version.

How do I authenticate with the Odoo API?

There are three main ways. API keys (Odoo 14+): generate a key in Settings > Users > API Keys and pass it as the password in XML-RPC/JSON-RPC calls or via the X-Api-Key header. Session authentication: POST to /web/session/authenticate with your database, login, and password to get a session cookie. Database authentication: call xmlrpc/2/common authenticate() with database, username, and password to get a uid, then use that uid for subsequent calls.

What is the difference between XML-RPC and JSON-RPC in Odoo?

XML-RPC is the legacy protocol available since Odoo 6 with the most documentation. JSON-RPC is the modern protocol used by the Odoo web client internally. Both expose identical methods (execute_kw, search_read, etc.) and have the same capabilities. JSON-RPC is preferred for new integrations because JSON is simpler to parse than XML, especially in JavaScript environments.

Can I use the Odoo API with JavaScript?

Yes. Use JSON-RPC with the fetch API or axios. POST to /jsonrpc with a JSON body containing the service, method, and arguments. For Node.js, you can also use the xmlrpc npm package for XML-RPC. JSON-RPC is simpler because you do not need an XML parsing library.

How do I handle pagination in Odoo API?

Use the limit and offset parameters in search_read. For example, limit=80, offset=0 returns the first 80 records. Set offset=80 for the next page. Call search_count first to know the total matching records, then divide by your page size to calculate total pages. Always include an order parameter (e.g., 'id asc') for consistent ordering across pages.

Deploy Odoo for API Testing in 5 Minutes

Spin up an isolated Odoo instance on your own cloud account. Test XML-RPC, JSON-RPC, and REST integrations against a real Odoo server with your own data. Free plan available.

Related Reading