Odoo has set dates for switching off the two protocols most integrations still use. The /xmlrpc, /xmlrpc/2 and /jsonrpc endpoints are deprecated since Odoo 19 and "scheduled for removal in Odoo 22 (fall 2028) and Online 21.1 (winter 2027)", per Odoo's External RPC API reference. Their replacement, the JSON-2 API, only exists from Odoo 19 (and Odoo Online 18.4). So an Odoo API integration built in late 2026 has to speak both: the RPC protocols for customers on Odoo 14 to 18, and JSON-2 for customers on 19 and later (updated October 2026).
The Odoo API is a model API over two protocols
Every external call in Odoo runs a method on a model: search_read on account.move for invoices, create on res.partner for a contact. The protocol only changes how the call travels.
| XML-RPC and JSON-RPC | JSON-2 | |
|---|---|---|
| Endpoint | /xmlrpc/2/common, /xmlrpc/2/object, /jsonrpc | POST /json/2/<model>/<method> |
| Auth | database, user ID and password or API key on every call | Authorization: bearer <api key> |
| Arguments | positional list plus keyword dict | named only, in a JSON body |
| Versions | Odoo 14 to 21 (older versions too, without API keys); Odoo Online before 21.1 | Odoo 19 and later (Online from 18.4) |
There is no REST API with resource URLs. An earlier version of this guide described a "native REST API" at /api/<model> in Odoo 17. Odoo's 17.0 external API docs and 18.0 docs document only XML-RPC (JSON-RPC exists at /jsonrpc but is not documented there), and the JSON-2 controller (addons/rpc/controllers/json2.py) first appears among major releases on Odoo's 19.0 branch.
Odoo 20.0 shipped in September 2026, and Odoo Online customers run intermediate SaaS releases (19.1 to 19.4 in 2026) that are "released every two to three months," per Odoo's support policy page. Ask each customer for their version, or read it from the server (both methods are below).

Plan and hosting requirements
Before writing any code, check the customer's plan. Both API references say: "Access to data via the external API is only available on Custom Odoo pricing plans. Access to the external API is not available on One App Free or Standard plans." Odoo's pricing page lists External API under the Custom plan and defines it as external software that "initiates calls to the Odoo API (JSON-RPC or XML-RPC)". Standard is sold for Odoo Online only, while Odoo.sh and on-premise Enterprise run on the Custom plan, so the restriction falls on Odoo Online customers on One App Free or Standard.
On Odoo Online the server URL is the instance domain (https://mycompany.odoo.com) and the database name is the instance name (mycompany). Odoo Online users also have no local password by default, so for password logins an administrator has to set one under Settings > Users & Companies > Users. An API key avoids that step.
Authentication with API keys
API keys arrived in Odoo 14, per the RPC reference. A user creates one under Preferences (or My Profile) > Account Security > New API Key, and Odoo shows it once. On XML-RPC and JSON-RPC the key replaces the password and the login stays the same. On JSON-2 the key goes in a bearer header and the login is not used at all.
From Odoo 18, keys expire. The JSON-2 reference says "it is not possible to create keys that last for more than three months," so long-lived keys "must be rotated at least once every three months." Odoo's source shows where the limit comes from: an api_key_duration of 90 days on the base internal user group, and only users with Settings access can still create a "Persistent Key" (base_groups.xml, res_users.py). Plan for rotation from day one.
Run the integration as a dedicated bot user rather than a person's account. Odoo recommends this because "the minimum required permissions can be granted to the bot," and every call is checked against "the access rights, record rules and field accesses of the user." On Odoo 19, keys can also be generated and revoked through res.users.apikeys/generate and res.users.apikeys/revoke, which by default only Settings administrators may call.
Calling the Odoo API with XML-RPC (Odoo 14 to 18)
XML-RPC needs two steps: authenticate on the common endpoint returns a user ID, and every call to execute_kw on the object endpoint carries the database, that ID and the key. This is the pattern from Odoo's RPC reference:
import xmlrpc.client
url = "https://mycompany.odoo.com"
db = "mycompany"
username = "bot@example.com"
password = "<api key>"
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
invoices = models.execute_kw(
db, uid, password,
'account.move', 'search_read',
[[['move_type', '=', 'out_invoice'], ['state', '=', 'posted']]],
{'fields': ['name', 'partner_id', 'amount_total', 'payment_state'], 'limit': 100, 'offset': 0, 'order': 'id asc'}
)
common.version() needs no login and returns the server version, which is the cheapest connection test. Always pass fields: without it, read and search_read return every field the user can read, 121 fields on a partner in Odoo's own example.
JSON-RPC uses the same services over JSON. The route in Odoo's source is POST /jsonrpc taking service, method and args (jsonrpc.py), so a call to object.execute_kw sends the same argument list as above. An earlier version of this guide posted to /web/dataset/call_kw instead; the external endpoint is /jsonrpc.
Calling the Odoo API with JSON-2 (Odoo 19 and later)
JSON-2 puts the model and method in the URL and every argument by name in the body. Adapted from the res.partner example in Odoo's JSON-2 reference:
import requests
BASE_URL = "https://mycompany.odoo.com/json/2"
API_KEY = ... # get it from a secure location
headers = {
"Authorization": f"bearer {API_KEY}",
"X-Odoo-Database": "mycompany",
"User-Agent": "mysoftware " + requests.utils.default_user_agent(),
}
res = requests.post(
f"{BASE_URL}/account.move/search_read",
headers=headers,
json={
"context": {"lang": "en_US"},
"domain": [["move_type", "=", "out_invoice"], ["state", "=", "posted"]],
"fields": ["name", "partner_id", "amount_total", "payment_state"],
},
)
res.raise_for_status()
invoices = res.json()
X-Odoo-Database is only required when one server hosts several databases and routing by host name isn't configured. Methods that act on records take an ids array, and positional arguments are gone: create takes vals_list and write takes vals, the parameter names in Odoo's ORM reference. The version check moves to GET /web/version, and each database documents its own models, fields and methods at /doc.
Each JSON-2 call runs in its own database transaction, and Odoo warns that "it is not possible to chain multiple calls inside a single transaction." Prefer one method that does the whole job: search_read instead of search then read, and action_post to post an invoice instead of writing the state yourself.
Errors come back as HTTP 4xx or 5xx with a JSON body holding the exception name, message, arguments, context and a debug traceback. A bad key returns 401 with "Invalid apikey". On XML-RPC the same exceptions surface as faults; the ones to handle are AccessError, ValidationError, UserError and MissingError, listed in the ORM reference.
Accounting models and what changed in Odoo 18 and 19
account.move holds customer invoices, vendor bills, credit notes, receipts and journal entries, split by move_type:
| move_type | Document |
|---|---|
out_invoice | Customer invoice |
in_invoice | Vendor bill |
out_refund | Customer credit note |
in_refund | Vendor credit note |
out_receipt | Sales receipt |
in_receipt | Purchase receipt |
entry | Journal entry |
state is draft, posted or cancel. On Odoo 19, payment_state can be not_paid, in_payment, paid, partial, reversed, blocked or invoicing_legacy (account_move.py). Lines live in account.move.line, and contacts for both sides are res.partner records with customer_rank and supplier_rank counters.
Creating an invoice uses Odoo's x2many command syntax. Over RPC you write the literal tuple, where 0 means create (ORM reference):
invoice_id = models.execute_kw(
db, uid, password,
'account.move', 'create',
[{
'move_type': 'out_invoice',
'partner_id': 42,
'invoice_date': '2026-10-01',
'invoice_line_ids': [
[0, 0, {'name': 'Consulting services', 'quantity': 10, 'price_unit': 150.0}]
],
}]
)
models.execute_kw(db, uid, password, 'account.move', 'action_post', [[invoice_id]])
Deleting is restricted once an entry has a number: unless the user has accounting admin rights, Odoo raises "You cannot delete this entry, as it has already consumed a sequence number and is not the last one in the chain," and companies with the restrictive audit trail can't delete posted entries at all. Reverse or cancel instead.
These model changes break code written for older versions:
| Model | Change | Version |
|---|---|---|
account.account | account_type selection replaces the user_type_id relation | 16.0 |
account.account | company_ids (many-to-many) replaces company_id | 18.0 |
account.payment | no longer inherits account.move; own state (draft, in_process, paid, canceled, rejected); memo replaces ref | 18.0 |
account.account | active replaces deprecated; new type expense_other | 19.0 |
The sources are Odoo's own model files for account.account in 19.0 and account.payment in 18.0. A filter like ['deprecated', '=', False] fails on Odoo 19, and ['state', '=', 'posted'] no longer matches any payment state on 18 and later. To pay an invoice, use Odoo's register-payment wizard model, account.payment.register, which reconciles the payment with the invoice. A bare account.payment record is not matched to anything.
Multi-company, pagination and rate limits
A multi-company database answers for the user's main company unless the call's context sets allowed_company_ids, per the Environment.company entry in the ORM reference. Odoo raises an AccessError when that list holds a company the user can't access. Set the company per call, or run one bot user per company.
Odoo doesn't paginate for you. Per the RPC reference, search returns every match unless you pass limit and offset, and search_count returns the total without loading records. Page with a stable order such as id asc.
Odoo publishes no numeric API rate limit. Its Acceptable Use Policy prohibits "unthrottled RPC/API calls" on Odoo's cloud and says "throttled calls are typically acceptable for unsustained usage at a rate of 1 call/second, with no parallel calls." On Odoo.sh, the dedicated hosting mode can lift that restriction. Size an initial sync on Odoo Online for one call per second.
Webhooks and incremental sync
Odoo has no webhook subscription API. From Odoo 17, automation rules include a "Send Webhook Notification" action that sends "a POST request with the values of the Fields to the URL" when a trigger fires, per the automation rules docs. An administrator configures it in Studio, which on Standard moves the database to the Custom plan. Odoo's webhooks page, written for incoming webhooks, advises testing on a duplicate database first.
For an integration you control end to end, poll on write_date, which "stores when the record was last updated":
updated = models.execute_kw(
db, uid, password,
'account.move', 'search_read',
[[['write_date', '>=', '2026-10-22 00:00:00'], ['move_type', '=', 'out_invoice']]],
{'fields': ['name', 'state', 'payment_state', 'write_date'], 'order': 'write_date asc'}
)
Store the highest write_date you saw and use it as the next lower bound.
Testing
https://demo.odoo.com/start hands out a temporary database and returns its host, database, user and password, per the RPC reference. It's a quick way to check calls against a current Odoo version. For accounting work, test against a copy of a real customer database on the same version, since installed modules decide which models exist.
Building on Apideck instead
A direct Odoo API integration means two protocol paths, version branches on the accounting models and key rotation every three months for non-admin users. Apideck's Odoo API integration puts Odoo behind the same Accounting API as the other ledgers it supports, and Odoo is also live on Apideck's CRM API. The Odoo connector page lists 28 supported accounting resources, including invoices, bills, payments, credit notes, journal entries and ledger accounts, with virtual webhooks for created and updated events. Customers connect with their subdomain, login and API key, and the same Custom plan requirement applies on Odoo Online. Apideck's connection guide adds that a self-hosted instance on its own custom domain, with no odoo.com subdomain, can't be connected.
The connector page also lists what it doesn't do: list responses carry no total count, and a connection is scoped to one Odoo company. Our build vs buy guide for accounting integrations walks through that trade-off in more detail.
If Odoo is the only ERP you integrate and your customers are on Odoo 19 or later, build directly on JSON-2 with a dedicated bot user and a key rotation job. If Odoo is one of several ledgers your customers use, or they span Odoo 14 to 20, start with Apideck and keep direct Odoo code for the models the unified API doesn't cover.
Ready to get started?
Scale your integration strategy and deliver the integrations your customers need in record time.








