Odoo API Integration: REST, JSON-RPC, and XML-RPC Guide

Complete developer guide to Odoo API integration using REST, JSON-RPC, and XML-RPC. Includes authentication, endpoints, code examples, and best practices.

E
ECOSIRE Research and Development Team
|March 19, 202610 min read2.1k Words|

Odoo API Integration: REST, JSON-RPC, and XML-RPC Guide

Odoo 19 exposes three API interfaces that cover everything from simple data retrieval to complex workflow automation. Whether you're building a custom mobile app, syncing with a third-party platform, or extending Odoo's capabilities with external microservices, mastering the Odoo API layer is foundational to any serious integration project.

This guide provides working code examples, authentication flows, and architectural recommendations for REST, JSON-RPC, and XML-RPC integrations — the three primary interfaces available in Odoo 19 Enterprise.

Key Takeaways

  • Odoo 19 offers REST (OpenAPI 3.0), JSON-RPC 2.0, and XML-RPC interfaces
  • Authentication uses API keys (recommended) or session-based login
  • JSON-RPC is the most feature-complete interface for complex operations
  • REST API follows OpenAPI 3.0 spec and supports standard HTTP verbs
  • XML-RPC is legacy but still fully supported for backward compatibility
  • Rate limiting and error handling must be implemented on the client side
  • Webhooks in Odoo 19 push data to external systems on record changes
  • All API calls respect Odoo's access rights and record rules

API Interface Comparison

Before writing a single line of code, choose the right API interface for your use case:

FeatureREST APIJSON-RPCXML-RPC
ProtocolHTTP/HTTPSHTTP/HTTPSHTTP/HTTPS
Payload formatJSONJSONXML
OpenAPI specYes (Swagger)NoNo
CRUD operationsYesYesYes
Method callsLimitedFullFull
Workflow triggersVia actionsVia execute_kwVia execute
Recommended forNew integrationsComplex logicLegacy systems
Python libraryrequestsodoo-xmlrpc / requestsxmlrpc.client

When to use REST: Building a mobile app, integrating with webhook-native platforms (Shopify, Stripe), or when your team is more comfortable with REST conventions.

When to use JSON-RPC: Executing complex Odoo server-side methods, reading large datasets with domain filters, or when you need access to methods not exposed via REST.

When to use XML-RPC: Maintaining existing integrations built before REST was available, or when your platform has mature XML-RPC client libraries.


Authentication

Odoo 19 supports API key authentication for all three interfaces. Generate an API key under Settings → Users → Your User → API Keys.

import requests

ODOO_URL = "https://your-odoo.com"
API_KEY = "your_api_key_here"
DATABASE = "your_database"

headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {API_KEY}"
}

API keys are scoped to a specific user and inherit that user's access rights. Create dedicated service users with minimal required permissions for integration accounts.

Session-Based Authentication (JSON-RPC / XML-RPC)

For JSON-RPC, authenticate using the /web/dataset/call_kw endpoint after establishing a session:

import requests
import json

session = requests.Session()

# Authenticate
auth_payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "db": "your_database",
        "login": "admin",
        "password": "your_password"
    }
}

response = session.post(
    f"{ODOO_URL}/web/session/authenticate",
    json=auth_payload
)
uid = response.json()['result']['uid']
print(f"Authenticated as UID: {uid}")

For XML-RPC, use the standard two-step authentication:

import xmlrpc.client

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

# Step 1: Get UID
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})

# Step 2: Use UID for subsequent calls
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")

REST API: OpenAPI 3.0

Odoo 19 introduces a full REST API with OpenAPI 3.0 specification. Access the interactive documentation at https://your-odoo.com/api/docs.

Listing Records

# GET /api/sale.order — list all sales orders
response = requests.get(
    f"{ODOO_URL}/api/sale.order",
    headers=headers,
    params={
        "domain": '[["state", "=", "sale"]]',
        "fields": '["name", "partner_id", "amount_total", "state"]',
        "limit": 50,
        "offset": 0
    }
)
orders = response.json()

Reading a Single Record

# GET /api/sale.order/{id}
order_id = 123
response = requests.get(
    f"{ODOO_URL}/api/sale.order/{order_id}",
    headers=headers
)
order = response.json()

Creating a Record

# POST /api/sale.order
payload = {
    "partner_id": 42,
    "order_line": [
        {
            "product_id": 7,
            "product_uom_qty": 5,
            "price_unit": 100.0
        }
    ]
}
response = requests.post(
    f"{ODOO_URL}/api/sale.order",
    headers=headers,
    json=payload
)
new_order = response.json()

Updating a Record

# PATCH /api/sale.order/{id}
response = requests.patch(
    f"{ODOO_URL}/api/sale.order/{order_id}",
    headers=headers,
    json={"note": "Rush order — priority handling required"}
)

Deleting a Record

# DELETE /api/sale.order/{id}
response = requests.delete(
    f"{ODOO_URL}/api/sale.order/{order_id}",
    headers=headers
)

JSON-RPC Interface

JSON-RPC provides access to the full Odoo Python API, including server-side methods that are not exposed via REST. The primary endpoint is /web/dataset/call_kw.

Basic Search and Read

def call_kw(model, method, args, kwargs=None):
    payload = {
        "jsonrpc": "2.0",
        "method": "call",
        "params": {
            "model": model,
            "method": method,
            "args": args,
            "kwargs": kwargs or {}
        }
    }
    response = session.post(
        f"{ODOO_URL}/web/dataset/call_kw",
        json=payload
    )
    return response.json().get('result')

# Search for confirmed sales orders
order_ids = call_kw(
    "sale.order",
    "search",
    [[["state", "=", "sale"]]],
    {"limit": 100, "order": "date_order desc"}
)

# Read specific fields
orders = call_kw(
    "sale.order",
    "read",
    [order_ids],
    {"fields": ["name", "partner_id", "amount_total", "date_order"]}
)

Search Read (Combined)

orders = call_kw(
    "sale.order",
    "search_read",
    [[["partner_id.country_id.code", "=", "US"]]],
    {
        "fields": ["name", "partner_id", "amount_total"],
        "limit": 50,
        "offset": 0,
        "order": "amount_total desc"
    }
)

Creating Records

new_id = call_kw(
    "sale.order",
    "create",
    [{
        "partner_id": 42,
        "order_line": [
            (0, 0, {
                "product_id": 7,
                "product_uom_qty": 10,
                "price_unit": 150.0
            })
        ]
    }]
)

Calling Server-Side Methods

JSON-RPC gives access to all Python methods defined on Odoo models:

# Confirm a sales order (triggers workflow)
call_kw("sale.order", "action_confirm", [[order_id]])

# Validate an inventory transfer
call_kw("stock.picking", "button_validate", [[picking_id]])

# Get the action for a button (useful for understanding what a button does)
action = call_kw("sale.order", "action_quotations_with_onboarding", [[]])

XML-RPC Interface

XML-RPC is the original Odoo API and remains fully supported. The interface consists of two endpoints:

  • /xmlrpc/2/common — unauthenticated methods (authenticate, version)
  • /xmlrpc/2/object — all model operations (requires UID)
import xmlrpc.client

url = "https://your-odoo.com"
db, username, password = "mydb", "admin", "mypassword"

common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")

# Search for products
product_ids = models.execute_kw(
    db, uid, password,
    'product.template', 'search',
    [[['sale_ok', '=', True]]],
    {'limit': 100}
)

# Read product data
products = models.execute_kw(
    db, uid, password,
    'product.template', 'read',
    [product_ids],
    {'fields': ['name', 'list_price', 'categ_id']}
)

# Create a new product
new_product_id = models.execute_kw(
    db, uid, password,
    'product.template', 'create',
    [{
        'name': 'My New Product',
        'list_price': 99.99,
        'type': 'consu'
    }]
)

Domain Filters

Odoo's domain filter syntax is used across all three API types. Understanding domains is essential for efficient data retrieval.

# Basic operators: =, !=, >, <, >=, <=, like, ilike, in, not in, child_of
domain = [
    ["state", "in", ["sale", "done"]],      # Confirmed or done orders
    ["amount_total", ">=", 1000],            # Total at least 1000
    ["partner_id.country_id.code", "=", "US"] # US customers (related field)
]

# Logical operators: & (AND, default), | (OR), ! (NOT)
domain = [
    "|",
    ["state", "=", "draft"],
    ["state", "=", "cancel"]
]

# Complex: orders from US or UK customers with total > 5000
domain = [
    "|",
    ["partner_id.country_id.code", "=", "US"],
    ["partner_id.country_id.code", "=", "GB"],
    ["amount_total", ">", 5000]
]

Webhooks and Event-Driven Integration

Odoo 19 supports outbound webhooks triggered by record changes. Configure webhooks under Settings → Technical → Webhooks.

Webhook configuration:

  1. Navigate to Settings → Technical → Webhooks → Create
  2. Set the Model (e.g., sale.order)
  3. Select Trigger: create, write, unlink, or custom method
  4. Enter the Endpoint URL of your receiving service
  5. Optionally set Domain to filter which records trigger the webhook
  6. Configure Fields to include in the payload

Receiving webhook events in a Flask service:

from flask import Flask, request, jsonify
import hmac, hashlib

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

@app.route("/odoo-webhook", methods=["POST"])
def handle_webhook():
    # Verify signature
    signature = request.headers.get("X-Odoo-Signature")
    body = request.get_data()
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.json
    model = event.get("model")
    record_id = event.get("id")

    # Process the event
    if model == "sale.order":
        handle_order_event(record_id, event)

    return jsonify({"status": "ok"}), 200

Error Handling and Retry Logic

Robust integrations must handle Odoo API errors gracefully.

import time
import requests
from requests.exceptions import RequestException

def api_call_with_retry(url, payload, headers, max_retries=3, backoff=2):
    for attempt in range(max_retries):
        try:
            response = requests.post(url, json=payload, headers=headers, timeout=30)
            response.raise_for_status()

            data = response.json()
            if "error" in data:
                error = data["error"]
                code = error.get("code", 0)
                message = error.get("data", {}).get("message", "Unknown error")

                # Don't retry validation errors
                if code in [200, 100]:
                    raise ValueError(f"Odoo validation error: {message}")

                raise RuntimeError(f"Odoo API error {code}: {message}")

            return data.get("result")

        except (RequestException, RuntimeError) as e:
            if attempt == max_retries - 1:
                raise
            wait = backoff ** attempt
            print(f"Attempt {attempt + 1} failed: {e}. Retrying in {wait}s...")
            time.sleep(wait)

Common error codes:

CodeMeaningAction
100Server errorCheck Odoo logs
200Access deniedVerify user permissions
300Missing recordCheck record ID exists
304Missing required fieldReview payload

Performance Best Practices

Batch operations: Never call the API in a loop for individual records. Use create_multi and write with lists:

# Bad: loop with individual creates
for product in products:
    call_kw("product.template", "create", [product])

# Good: batch create
call_kw("product.template", "create", [products])

Field selection: Always specify the fields parameter to avoid fetching all fields:

# Good: only fetch needed fields
orders = call_kw(
    "sale.order", "search_read",
    [[["state", "=", "sale"]]],
    {"fields": ["name", "amount_total"], "limit": 1000}
)

Pagination: For large datasets, paginate using limit and offset:

def fetch_all_records(model, domain, fields, batch_size=500):
    records = []
    offset = 0
    while True:
        batch = call_kw(
            model, "search_read", [domain],
            {"fields": fields, "limit": batch_size, "offset": offset}
        )
        records.extend(batch)
        if len(batch) < batch_size:
            break
        offset += batch_size
    return records

Frequently Asked Questions

What is the difference between JSON-RPC and the REST API in Odoo 19?

JSON-RPC provides access to the complete Odoo Python API including all server-side methods, while REST follows OpenAPI 3.0 conventions and exposes a more limited but standardized interface. For new integrations where REST covers your use case, REST is preferred for its discoverability. For complex workflow automation or access to custom Python methods, use JSON-RPC.

How do I handle large data exports (100k+ records) efficiently?

Use pagination with search_read and a batch size of 500–1000 records. For very large exports, consider using Odoo's export feature via the UI for one-time extractions, or schedule background jobs using Odoo's ir.cron model to process data in chunks during off-peak hours rather than making real-time API calls.

Can I use API keys instead of username/password for XML-RPC?

Yes. In Odoo 13+, API keys can be used as passwords in the XML-RPC authenticate call. Generate an API key from your user profile and use it in place of your password: common.authenticate(db, username, api_key, {}). This is the recommended approach for service accounts.

How do I create Many2many and One2many records via the API?

Use Odoo's command tuples: (0, 0, vals) creates a new related record, (1, id, vals) updates an existing related record, (2, id, 0) deletes a related record, (4, id, 0) links an existing record, (5, 0, 0) unlinks all related records. These commands work identically across JSON-RPC, XML-RPC, and REST.

How do I trigger a workflow action (like confirming an order) via the API?

Call the corresponding method on the model. For confirming a sales order, call action_confirm on sale.order. For validating a delivery, call button_validate on stock.picking. These methods are visible in Odoo's source code and can be discovered by inspecting the button's name attribute in the UI's developer mode.

What rate limits does Odoo impose on API calls?

Odoo does not enforce API rate limits natively at the application level. Rate limiting must be configured at the reverse proxy (Nginx) or infrastructure level. A sensible default is 60 requests per minute per IP for external integrations. For high-throughput integrations, use a queue-based approach with a dedicated service user.


Next Steps

Building a reliable Odoo API integration requires more than working code examples — it demands proper error handling, monitoring, credential management, and alignment with Odoo's data model.

ECOSIRE's integration team has built production-grade connections between Odoo and dozens of platforms including Shopify, Amazon, GoHighLevel, Power BI, custom ERPs, and proprietary systems. We handle authentication architecture, webhook design, data transformation, and ongoing monitoring.

Talk to ECOSIRE About Your Odoo Integration Project →

Whether you're starting a new integration or fixing a broken one, our engineers will review your requirements and deliver a solution that handles edge cases from day one.

E

Written by

ECOSIRE Research and Development Team

Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.

Chat on WhatsApp