VERSICH

Odoo API Integration: A Complete Developer Guide

odoo api integration: a complete developer guide

Odoo API integration is the process of connecting Odoo to other applications through its external API, so records such as contacts, orders, and invoices can be read, created, and updated automatically instead of being copied by hand. 

Odoo rarely operates in isolation. An order may originate from an ecommerce platform; payments may be processed by a separate provider, and shipment updates may come from a warehouse management system. An Odoo API integration connects to these systems, so information can move between them without relying on manual exports, duplicate data entry, or disconnected workflows. 

The challenge is choosing the right integration method and making it reliable. An endpoint that works during a test may still fail when permissions change; a request times out, or an external service sends the same event twice. 

This guide explains how to approach Odoo API integration, with practical examples using Odoo 19's External JSON-2 API. It also covers migrating from legacy RPC integrations, rate limits, multi-company setups, security, error handling, and the design decisions that matter when moving beyond a proof of concept. 

1. Understand the available Odoo API options 

Before writing code, identify your Odoo version, hosting environment, and integration requirements. 

Odoo 19 JSON-2 API 

Odoo 19 introduces the External JSON-2 API. It exposes model methods through HTTP endpoints following this pattern: 

POST /json/2/<model>/<method

The client sends a JSON body and authenticates with an API key in a bearer authorization header. For example, an integration can call res.partner/search_read  to retrieve selected contact records. Odoo's own External JSON-2 API documentation is the reference for headers, request format, and error responses. 

JSON-2 is model-oriented. It does not work exactly like a conventional REST API that exposes a separate resource URL for each record. Instead, it provides access to Odoo model methods. 

XML-RPC and JSON-RPC 

Existing integrations may still use XML-RPC or JSON-RPC. However, Odoo documents these legacy external endpoints (/xmlrpc, /xmlrpc/2, and /jsonrpc) as deprecated and scheduled for removal. The Odoo 19 documentation names Odoo 20 (fall 2026) as the removal release, while some third-party articles still cite later dates. Check Odoo's current documentation for the date that applies to you, and plan to migrate rather than assume older tutorials remain the best starting point. 

Custom controllers 

A custom controller may be appropriate when an external application needs a purpose-built endpoint with a specific request format or workflow. For example, a shipping provider might need to submit a tracking event without receiving broad access to Odoo's general model API. 

A custom controller requires additional development and security review. It should expose only the operations the external system needs. If you need a purpose-built endpoint or an API layer in front of Odoo, our  API development services cover that kind of work. 

Choose your approach based on the requirement: 

  • Use JSON-2 for supported external model operations on Odoo 19 when the hosting plan permits external API access. 

  • Migrate legacy RPC integrations to JSON-2 before the legacy endpoints are removed. 

  • Consider a custom controller when the integration requires a narrow, purpose-built interface or behavior that the standard API does not provide. 

External API access is restricted by Odoo's pricing-plan rules. Odoo states that it is available only on Custom plans, not on One App Free or Standard plans. Check the current plan and deployment requirements before designing around JSON-2. 

2. Common Odoo API integration use cases 

Most Odoo integrations fall into a few patterns. Knowing which one you are building helps you decide which system owns each piece of data. 

  • Ecommerce: send orders from Shopify, WooCommerce, or Magento into Odoo, and push stock levels back to the store. 

  • Inventory and fulfillment: sync stock movements with warehouse, marketplace, or point-of-sale systems so stock counts agree across channels. 

  • CRM and customer data: keep contacts and leads consistent between Odoo and tools such as HubSpot or Salesforce. 

  • Accounting and finance: exchange invoices, payments, and journal entries with external accounting or expense tools. 

  • Reporting and analytics: extract sales and accounting data into BI tools for dashboards. 

  • Shipping and logistics: send confirmed orders to carriers and receive tracking updates. 

  • Mobile and internal apps: let field staff look up customers, check stock, or create orders from their own application. 

For each use case, decide whether the flow is one-way or two-way, and whether updates must be near real time or can run on a schedule. For a business-level, look at which systems growing companies usually connect first, see our guide to 5 Odoo integrations that help growing businesses connect operations.

3. Prepare Odoo for the integration 

A working endpoint is only one part of the setup. The integration also needs an account with appropriate access and a clear data contract. 

Create a dedicated integration user 

For a long-running integration, avoid using an employee's personal account or the administrator account. Create a dedicated user for the external service and grant only the permissions required for its tasks. 

Odoo recommends dedicated bot users for extended automated usage. A bot user limits the damage if its key is compromised, can have an empty password so login-and-password authentication is disabled, and appears in the access log under its own name instead of impersonating a person. 

For example, a contact-sync service may need to read and create contacts but should not automatically receive permission to access accounting records. 

Odoo's security rules still apply to JSON-2 requests. Access rights, record rules, and field restrictions can determine which records and fields the integration can read or modify. 

Generate an API key 

In Odoo 19, an API key can be created through the user's preferences under Account Security. A description and a duration are both required. The key is displayed only when it is created, so store it securely. 

Odoo does not allow keys that last longer than three months, so a long-running integration must rotate its key at least that often. Build key rotation into the integration from the start instead of treating it as a later task. 

For production integrations: 

  • Keep the key in a secret manager or protected environment variable. 

  • Use a separate key for each integration where practical. 

  • Choose a duration that matches the use. Short durations suit interactive testing; production keys must be rotated within three months. 

  • Revoke a compromised key immediately. 

  • Avoid placing credentials in source code, logs, or public repositories. 

Identify the model and fields 

Odoo model names are technical names, not necessarily the labels users see in the interface. Contacts, for example, use the res.partner model. Other commonly used models include: 

  • sale.order for sales orders. 

  • account.move for invoices and journal entries. 

  • stock.picking for deliveries, receipts, and internal transfers. 

  • hr.employee for employee records. 

The models, fields, and methods available are specific to each database, including any custom modules. Odoo notes that they can be consulted on the database's /doc page. You can also turn on developer mode, open a record, and use the developer tools to view its model name and field names. 

Before building the integration, confirm: 

  • The technical model name. 

  • The fields the integration must read or write. 

  • Any required fields and valid selection values. 

  • The relationships between the records. 

  • Which user permissions are necessary. 

  • Whether the integration should create a new record or update an existing one. 

4. Make your first JSON-2 request 

The following example uses Python and the requests library to retrieve selected contact fields from Odoo 19. 

Install the dependency if it is not already available: 

pip install requests 

Set the following environment variables before running the script: 

  • ODOO_URL: the base URL of the Odoo instance. 

  • ODOO_API_KEY: the API key for the dedicated integration user. 

  • ODOO_DATABASE: the database name, when the deployment requires it. 

Example: 

import os 

import requests 

  

ODOO_URL = os.environ["ODOO_URL"].rstrip("/") 

ODOO_API_KEY = os.environ["ODOO_API_KEY"] 

ODOO_DATABASE = os.getenv("ODOO_DATABASE") 

  

headers = { 

    "Authorization": f"bearer {ODOO_API_KEY}", 

    "Content-Type": "application/json", 

    "User-Agent": "versich-contact-sync/1.0", 

} 

  

if ODOO_DATABASE: 

    headers["X-Odoo-Database"] = ODOO_DATABASE 

  

payload = { 

    "domain": [["is_company", "=", True]], 

    "fields": ["id", "name", "email"], 

    "limit": 20, 

    "order": "name asc", 

} 

  

response = requests.post( 

    f"{ODOO_URL}/json/2/res.partner/search_read", 

    headers=headers, 

    json=payload, 

    timeout=30, 

) 

  

response.raise_for_status() 

contacts = response.json() 

  

for contact in contacts: 

    print( 

        contact["id"], 

        contact.get("name"), 

        contact.get("email"), 

    )

This request searches for company contacts and returns only the requested fields. The limit keeps the example small and makes the response easier to inspect. 

Notice that every parameter in the body is named (domain, fields, limit, order). JSON-2 has no positional arguments. Two keys are common to most calls: ids lists the records a method should act on, and context passes extra values such as {"lang": "en_US"}. 

The optional X-Odoo-Database header is needed when a single Odoo server hosts several databases and the database cannot be worked out from the host name; it is not required in every environment. Use the actual database and domain configuration for your instance. 

Important: This is a starting example, not a complete production sync. It retrieves data but does not track which records have already been processed, handle pagination across a large dataset, or resolve conflicts between systems. 

5. Create, read, update, and delete records 

Odoo model methods support common data operations, but the exact parameters depend on the method and model. 

Read records 

Use search_read  when you need to find matching records and retrieve their fields in one call.

payload = { 

    "domain": [["email", "!=", False]], 

    "fields": ["id", "name", "email"], 

    "limit": 100, 

} 

  

response = requests.post( 

    f"{ODOO_URL}/json/2/res.partner/search_read", 

    headers=headers, 

    json=payload, 

    timeout=30, 

) 

response.raise_for_status() 

contacts = response.json() 

For large datasets, do not assume one request will return everything. Use a deliberate pagination or batching strategy appropriate to the model and method. Avoid repeatedly requesting the entire dataset when only recent changes are needed. 

Create a record 

In Odoo 19, the create method takes a list of value objects named vals_list  and returns a list of the new record IDs, in order. Passing one object in the list creates one record; passing several creates several in a single call. 

payload = { 

    "vals_list": [{ 

        "name": "Northstar Supplies", 

        "email": "purchasing@example.com", 

        "company_type": "company", 

    }] 

} 

  

response = requests.post( 

    f"{ODOO_URL}/json/2/res.partner/create", 

    headers=headers, 

    json=payload, 

    timeout=30, 

) 

response.raise_for_status() 

contact_id = response.json()[0] 

The [0, 0, {...}] entry is Odoo's command for "create a new line with these values". To confirm the order, call the model's own business method instead of writing the state field directly: 

response = requests.post( 

    f"{ODOO_URL}/json/2/sale.order/action_confirm", 

    headers=headers, 

    json={"ids": [order_id]}, 

    timeout=30, 

) 

response.raise_for_status()

Calling  action_confirm  lets Odoo run its normal checks and side effects. Writing  state  directly would skip them.

Update a record 

For an existing record, pass its ID and the fields to update. 

payload = { 

    "ids": [contact_id], 

    "vals": { 

        "phone": "+1-555-0100", 

    }, 

} 

  

response = requests.post( 

    f"{ODOO_URL}/json/2/res.partner/write", 

    headers=headers, 

    json=payload, 

    timeout=30, 

) 

The returned value indicates whether the operation succeeded, but a successful write does not necessarily prove that every downstream workflow behaved as expected. If the integration triggers dependent processes, test those processes too.

Delete a record 

Odoo exposes unlink operations, but deletion should not be treated as a routine sync action without a clear business rule. 

For instance, a contact removed from a marketing platform may still be needed for invoices or historical transactions. In many integrations, archiving the record (writing  active  as false on models that support it) or using an explicit synchronization status is safer than deleting the Odoo record. 

Before enabling deletion, define who owns the record, what happens to linked records, and how the operation can be audited or recovered. 

Work with multiple companies 

Odoo supports databases that hold several companies. The integration user's allowed companies and the company context of each request determine which records are visible and which company a new record belongs to. 

If your database has more than one company, set the company context deliberately on every request, for example by passing  allowed_company_ids inside  context and test with a user restricted to the intended companies. Leaving it unspecified can cause access errors or records created under the wrong company.

6. Migrate from XML-RPC or JSON-RPC to JSON-2 

If your integration still uses the legacy endpoints, migrate it before they are removed. The change is mostly mechanical, but a few differences cause most of the bugs. 

Legacy RPC 

JSON-2

Log in with database, login, and password, and receive a user ID 

Send an API key in the Authorization: bearer header; no user ID or password is needed 

Call execute_kw on the object service with database, user ID, password, model, and method 

POST /json/2/<model>/<method> 

Positional arguments 

Named arguments only, in a JSON body 

Record IDs as the first positional argument 

The ids key 

Context as a keyword argument 

The context key 

Database name on every call 

X-Odoo-Database header, only when the server needs it 

version() on the common service 

GET /web/version 

If you need the current user's ID, callres.users/context_getwith no IDs; Odoo identifies the user from the API key. 

A legacy read looks like this: 

import xmlrpc.client 

common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") 

uid = common.authenticate(db, username, password, {}) 

 
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") 

partners = models.execute_kw( 

    db, uid, password, 

    "res.partner", "search_read", 

    [[["is_company", "=", True]]], 

    {"fields": ["name", "email"], "limit": 20}, 

)

The same read with JSON-2: 

response = requests.post( 

    f"{ODOO_URL}/json/2/res.partner/search_read", 

    headers=headers, 

    json={ 

        "domain": [["is_company", "=", True]], 

        "fields": ["name", "email"], 

        "limit": 20, 

    }, 

    timeout=30, 

) 

Migrate one integration at a time and compare results against the legacy version on a staging database. Pay particular attention to calls that relied on positional arguments, because each one must be rewritten with the method's parameter names. The database management functions of the old db service are not part of JSON-2; Odoo's documentation lists the replacements. 

7. Handle errors without corrupting the sync 

An API integration needs to distinguish between a request that failed before reaching Odoo and a request whose outcome is uncertain. 

Use a timeout and inspect HTTP errors: 

try: 

    response = requests.post( 

        f"{ODOO_URL}/json/2/res.partner/search_read", 

        headers=headers, 

        json=payload, 

        timeout=30, 

    ) 

    response.raise_for_status() 

    result = response.json() 

  

except requests.Timeout: 

    # The request may have reached Odoo. 

    # Do not blindly repeat a non-idempotent operation. 

    raise 

  

except requests.HTTPError as exc: 

    status = exc.response.status_code 

    body = exc.response.text 

    raise RuntimeError( 

        f"Odoo API request failed: HTTP {status}: {body}" 

    ) from exc 

  

except requests.RequestException as exc: 

    raise RuntimeError( 

        f"Could not complete the Odoo API request: {exc}" 

    ) from exc

This example surfaces the error instead of silently continuing. A production service should classify errors, record enough diagnostic context, and decide whether the operation should be retried, corrected, or escalated. 

When a request fails, Odoo returns a 4xx or 5xx status with a JSON error object. It includes the name of the Python exception, a message, the exception arguments, the context used, and a debug field with the server traceback. Log the name and message for diagnosis, but do not pass the debug traceback to end users or store it where it could expose internal details. 

Common cases include: 

  • 401 Unauthorized: the API key may be missing, invalid, expired, or malformed. 

  • 403 Forbidden: the user may lack the required permission or the operation may be restricted. 

  • 404 Not Found: the URL, model, or method may be incorrect. 

  • 4xx validation errors: the request may contain invalid fields, values, or parameters. 

  • 5xx errors: Odoo or an upstream component may have encountered a server-side failure. 

  • Timeouts: the client may not know whether the server completed the operation. 

Do not log API keys or sensitive payloads while debugging. Also avoid assuming that every server error is safe to retry. 

8. Prevent duplicate records and conflicting updates 

Consider an ecommerce integration that sends an order to Odoo. The client submits the order, but the connection times out before it receives the response. If the client sends the same create request again, it could create a duplicate. 

A timeout alone cannot tell the client whether the first operation succeeded. 

A safer design uses an external identifier, such as the ecommerce platform's order ID, and a defined duplicate-handling rule. Depending on the data model, that may involve a dedicated integration field, a database uniqueness constraint, or a custom method that checks and creates records in a controlled operation. 

A simple search-before-create check can help, but it is not sufficient for concurrent requests unless the uniqueness rule is enforced at the appropriate level. Two requests can both search, find nothing, and attempt to create the same record. 

Remember that each call is its own transaction 

Every JSON-2 call runs in its own database transaction. It is committed on success and rolled back on error, and you cannot chain several calls into one transaction. If your integration makes a search call followed by a create call, another process can change the data between them. 

When several steps must succeed or fail together, such as reserving stock or confirming a payment, do not coordinate them from the client. Call a single model method that performs all the steps in one transaction. Odoo's business methods, usually prefixed with action_ , are examples. If no suitable method exists, a custom module can add one. Our walkthrough on how to build custom module in Odoo shows the structure such module needs. 

Also decide which system owns each field. If Odoo owns the customer address while the ecommerce platform owns the external order status, the integration should not overwrite those values indiscriminately. 

For complex workflows, an integration queue or middleware service can keep retries, reconciliation, and operational monitoring separate from the user-facing application. 

9. Respect rate limits and design for large volumes 

A script that processes 20 contacts is not automatically suitable for 200,000 records. 

Plan for rate limits 

Integration vendors report that Odoo applies acceptable-use limits to API calls, commonly described as roughly one call per second with no sustained parallel calls, and that requests beyond that may be throttled. Odoo's JSON-2 documentation does not state a figure, so confirm the limits that apply to your plan and deployment with Odoo or your hosting provider before you design for volume. 

Whatever the limit is, design for it: 

  • Do not run parallel workers against the same database without checking the limits. 

  • Back off and retry when a request is throttled, instead of retrying immediately. 

  • Batch work into fewer calls. For example, create accepts a list in vals_list, so one call can create many records. 

  • Request only the fields and records you need. 

Scale the integration 

For larger integrations: 

  1. Retrieve only the fields the external system needs. 

  1. Use bounded batches and respect the target method's behavior. 

  1. Store a reliable checkpoint or synchronization cursor. 

  1. Use incremental updates where the data model supports them. 

  1. Record failed items separately so one bad record does not stop the entire job. 

  1. Monitor response time, error rates, and records processed. 

  1. Test how the integration behaves when interrupted and restarted. 

Do not assume every model provides a reliable write_date or that it captures every event your integration needs. Confirm the field's semantics and use a durable event or reconciliation strategy where missing updates would be costly. 

For integrations that connect ERP data with ecommerce, shipping, accounting, or other operational systems, see our Odoo integration services. 

10. Test before production 

Use a development or staging database with representative data. Test more than the successful path. 

Set up a safe test environment 

  • Use a separate database, with its own integration user and API key, for testing. Never test against production data. 

  • Create sample records for the models you will touch, such as contacts, products, and sales orders. 

  • Run your calls from a script, or try them first with a tool such as Postman or curl. 

  • After each call, open the Odoo interface and confirm the record changed as expected. A successful response does not prove the business result is right. 

Cases to include 

  • Invalid credentials and expired keys. 

  • Users with insufficient access rights. 

  • Missing required fields and invalid values. 

  • Large result sets and interrupted requests. 

  • Duplicate create requests. 

  • Calls made with the wrong company context in a multi-company database. 

  • Throttled requests and backoff behavior. 

  • External service downtime. 

  • Changes to the Odoo model or a custom module. 

  • Reconciliation after an integration has been paused. 

Keep credentials outside the codebase, use version control, and document how to rotate keys and recover a failed sync. 

11. Build it yourself or use middleware? 

You can build an Odoo integration directly against JSON-2, or place a middleware layer, integration platform, or unified API between your application and Odoo. Neither is always better. 

Building directly tends to suit you when: 

  • You connect to Odoo and only one or two other systems. 

  • You need control over custom models, custom fields, or specific business methods. 

  • Your team can maintain the code, key rotation, retries, and monitoring. 

Middleware or an integration platform tends to suit you when: 

  • You need to connect many different ERPs or accounting systems through one interface. 

  • You want built-in retries, logging, and scheduling instead of building them. 

  • You lack the in-house capacity to maintain the integration over time. 

Before choosing a platform, check how it handles custom Odoo modules, which Odoo versions and hosting types it supports, who holds the credentials, where your data passes through, how it deals with rate limits, and what happens when Odoo changes its API. A third-party layer does not remove the need for a dedicated Odoo user, least-privilege access, and a plan for duplicates and failures. 

Conclusion 

A reliable Odoo API integration takes more than a request that returns data. On Odoo 19, build new integrations on the External JSON-2 API, authenticate with a short-lived API key held by a dedicated, least-privilege user, and migrate any XML-RPC or JSON-RPC code before Odoo removes the legacy endpoints. 

The practices that keep an integration trustworthy after launch are the less visible ones. Treat a timeout as an uncertain outcome, protect against duplicates with an external identifier and a uniqueness rule, call a single business method when several steps must succeed together, set the company context in multi-company databases, respect rate limits, and test the failure cases on a staging database before anything reaches production. 

Start small. Pick one data flow, decide which system owns each field, build it against a test database, and add monitoring and reconciliation before you expand. Confirm anything that depends on your plan or version, such as API availability, rate limits, and the removal date for the legacy endpoints, against Odoo’s current documentation.

Need help with an Odoo integration?

Versich can help scope and implement integrations as part of its Odoo integration services,

Get Started
CTA Illustration

Frequently Asked Questions

Does Odoo have a REST API?

Odoo 19 provides the External JSON-2 API over HTTP, but it is model-oriented rather than a conventional resource-based REST interface. Older versions commonly use XML-RPC or JSON-RPC for external model access.

Can I integrate Odoo with an external application using Python?

Yes. Python's requests library can call supported Odoo endpoints. The integration still needs appropriate authentication, permissions, error handling, and a design for retries and data consistency.

Can an Odoo API key access every record?

No. Requests are subject to the permissions of the associated user, including access rights, record rules, and field restrictions.

Is the Odoo API free, and which plans include it?

Odoo's documentation states that access to the external API is available only on Custom pricing plans, not on One App Free or Standard plans. Confirm the current plan and deployment rules before implementation.

What are the Odoo API rate limits?

Integration vendors report an acceptable-use guideline of about one call per second with no sustained parallel calls, but Odoo's JSON-2 page does not publish a figure. Confirm the limits for your plan, batch your requests, and back off when throttled.

Does the Odoo API support multi-company databases?

Yes, but each request should carry the correct company context, and the integration user must be allowed to access those companies. Otherwise you may see access errors or records created under the wrong company.

How do I migrate from XML-RPC or JSON-RPC to JSON-2?

Replace login and password authentication with an API key, send requests to /json/2/<model>/<method>, and rewrite positional arguments as named parameters in a JSON body. Test each integration on a staging database first.

Should I use a webhook or a scheduled API sync?

Use webhooks when you need event-driven updates and the systems can reliably deliver and process events. Scheduled synchronization can be easier to reconcile and recover when missing an update would be costly. Some integrations use both: webhooks for timely changes and scheduled jobs for reconciliation.