mirror of
https://github.com/odoo/documentation.git
synced 2025-12-12 07:29:27 +07:00
551 lines
21 KiB
ReStructuredText
551 lines
21 KiB
ReStructuredText
===================
|
|
External JSON-2 API
|
|
===================
|
|
|
|
.. versionadded:: 19.0
|
|
|
|
Odoo is usually extended internally via modules, but many of its features and all of its data are
|
|
also available externally for analysis or integration with various other softwares. Part of the
|
|
:ref:`reference/orm/model` API is easily available over HTTP via the ``/json/2`` endpoint.
|
|
|
|
.. tip::
|
|
The actual models, fields and methods available are specific to every database and can be
|
|
consulted on their ``/doc`` page.
|
|
|
|
.. note::
|
|
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. For more information
|
|
visit the `Odoo pricing page <https://www.odoo.com/pricing-plan>`_ or reach out to your Customer
|
|
Success Manager.
|
|
|
|
API
|
|
===
|
|
|
|
.. _reference/external_api/request:
|
|
|
|
Request
|
|
-------
|
|
|
|
Post a JSON object at the ``/json/2/<model>/<method>`` URL.
|
|
|
|
**HTTP Headers**
|
|
|
|
:Host: Required, the hostname of the server.
|
|
:Autorization: Required, ``bearer`` followed by an :ref:`API key <reference/external_api/api_key>`.
|
|
:Content-Type: Required, ``application/json``, a charset is recommended.
|
|
:X-Odoo-Database: Optional, the name of the database to connect to.
|
|
:User-Agent: Recommended, the name of your software.
|
|
|
|
**URL Path**
|
|
|
|
:model: Required, the technical model name.
|
|
:method: Required, the method to execute.
|
|
|
|
**Body JSON object**
|
|
|
|
:ids: An array of record ids on which to execute the method. Empty or omitted when calling an
|
|
``@api.model``-decorated method.
|
|
:context: Optional, an object of additional values. e.g. ``{"lang": "en_US"}``.
|
|
:*param*: As many time as needed, the method's parameters.
|
|
|
|
.. example::
|
|
.. code:: http
|
|
|
|
POST /json/2/res.partner/search_read HTTP/1.1
|
|
Host: mycompany.example.com
|
|
X-Odoo-Database: mycompany
|
|
Authorization: bearer 6578616d706c65206a736f6e20617069206b6579
|
|
Content-Type: application/json; charset=utf-8
|
|
User-Agent: mysoftware python-requests/2.25.1
|
|
|
|
{
|
|
"context": {
|
|
"lang": "en_US"
|
|
},
|
|
"domain": [
|
|
["name", "ilike", "%deco%"],
|
|
["is_company", "=", true]
|
|
],
|
|
"fields": ["name"]
|
|
}
|
|
|
|
.. _reference/external_api/response:
|
|
|
|
Response
|
|
--------
|
|
|
|
In case of **success**, a **200** status with the JSON-serialized return value of the called method
|
|
in the body.
|
|
|
|
.. example::
|
|
.. code:: http
|
|
|
|
HTTP/1.1 200 OK
|
|
Content-Type: application/json; charset=utf-8
|
|
|
|
[
|
|
{"id": 25, "name": "Deco Addict"}
|
|
]
|
|
|
|
In case of **error**, a **4xx**/**5xx** status with a JSON-serialized error object in the body.
|
|
|
|
:name: The fully qualified name of the Python exception that occured.
|
|
:message: The exception message, usually the same as `arguments[0]`.
|
|
:arguments: All the exception arguments.
|
|
:context: The context used by the request.
|
|
:debug: The exception traceback, for debugging purpose.
|
|
|
|
.. example::
|
|
|
|
.. tabs::
|
|
|
|
.. tab:: HTTP
|
|
.. code:: http
|
|
|
|
HTTP/1.1 401 Unauthorized
|
|
Content-Type: application/json; charset=utf-8
|
|
|
|
{
|
|
"name": "werkzeug.exceptions.Unauthorized",
|
|
"message": "Invalid apikey",
|
|
"arguments": ["Invalid apikey", 401],
|
|
"context": {},
|
|
"debug": "Traceback (most recent call last):\n File \"/opt/Odoo/community/odoo/http.py\", line 2212, in _transactioning\n return service_model.retrying(func, env=self.env)\n File \"/opt/Odoo/community/odoo/service/model.py\", line 176, in retrying\n result = func()\n File \"/opt/Odoo/community/odoo/http.py\", line 2177, in _serve_ir_http\n self.registry['ir.http']._authenticate(rule.endpoint)\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 274, in _authenticate\n cls._authenticate_explicit(auth)\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 283, in _authenticate_explicit\n getattr(cls, f'_auth_method_{auth}')()\n File \"/opt/Odoo/community/odoo/addons/base/models/ir_http.py\", line 240, in _auth_method_bearer\n raise werkzeug.exceptions.Unauthorized(\nwerkzeug.exceptions.Unauthorized: 401 Unauthorized: Invalid apikey\n"
|
|
}
|
|
|
|
.. tab:: Debug
|
|
.. code::
|
|
|
|
Traceback (most recent call last):
|
|
File "/opt/Odoo/community/odoo/http.py", line 2212, in _transactioning
|
|
return service_model.retrying(func, env=self.env)
|
|
File "/opt/Odoo/community/odoo/service/model.py", line 176, in retrying
|
|
result = func()
|
|
File "/opt/Odoo/community/odoo/http.py", line 2177, in _serve_ir_http
|
|
self.registry['ir.http']._authenticate(rule.endpoint)
|
|
File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 274, in _authenticate
|
|
cls._authenticate_explicit(auth)
|
|
File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 283, in _authenticate_explicit
|
|
getattr(cls, f'_auth_method_{auth}')()
|
|
File "/opt/Odoo/community/odoo/addons/base/models/ir_http.py", line 240, in _auth_method_bearer
|
|
raise werkzeug.exceptions.Unauthorized(
|
|
werkzeug.exceptions.Unauthorized: 401 Unauthorized: Invalid apikey
|
|
|
|
Configuration
|
|
=============
|
|
|
|
.. _reference/external_api/api_key:
|
|
|
|
API Key
|
|
-------
|
|
|
|
An API key must be set in the ``Authorization`` request header, as a bearer token.
|
|
|
|
Create a new API key for a user via :menuselection:`Preferences --> Account Security -->
|
|
New API Key`.
|
|
|
|
.. have the three images appear next to each other
|
|
.. list-table::
|
|
|
|
* - .. image:: external_api/preferences2.png
|
|
:align: center
|
|
|
|
- .. image:: external_api/account-security2.png
|
|
:align: center
|
|
|
|
- .. image:: external_api/new-api-key.png
|
|
:align: center
|
|
|
|
Both a description and a duration are needed to create a new API key. The description makes it
|
|
possible to identify the key, and to determine later whether the key is still in use or should be
|
|
removed. The duration determines the lifetime of the key, after which the key becomes invalid. It is
|
|
recommended to set a short duration (typically one day) for interactive usage. For security reasons,
|
|
it is not possible to create keys that last for more than three months. This means that long lasting
|
|
keys must be rotated at least once every three months.
|
|
|
|
The :guilabel:`Generate Key` button creates a strong 160-bits random key. The key value is displayed
|
|
only once during creation and cannot be retrieved later. Copy the key immediately and store it
|
|
securely. If the key is compromised or lost, delete it immediately and generate a new one.
|
|
|
|
Please refer to `OWASP's Secrets Management Cheat Sheet
|
|
<https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html#secrets-management-cheat-sheet>`_
|
|
for further guidance on the management of API keys.
|
|
|
|
.. _reference/external_api/access_rights:
|
|
|
|
Access Rights
|
|
-------------
|
|
|
|
The JSON-2 API uses the standard :ref:`security models of Odoo <reference/security>`. All operations
|
|
are validated against the access rights, record rules and field accesses of the user.
|
|
|
|
For **interactive usage**, such as discovering the API or running one-time scripts, it is fine to
|
|
use a **personal account**.
|
|
|
|
For **extended automated usage**, such as an integration with another software, it is recommended to
|
|
create and use **dedicated bot users**. Using dedicated bot users has several benefits:
|
|
|
|
* The minimum required permissions can be granted to the bot, limiting the impact if the API key
|
|
gets compromised.
|
|
* The password can be set empty to disable login/password authentication, limiting the likelihood
|
|
of the account getting compromised.
|
|
* The :ref:`reference/fields/automatic/log_access` use the bot account. No user is impersonalized.
|
|
|
|
.. _reference/external_api/database:
|
|
|
|
Database
|
|
--------
|
|
|
|
Depending on the deployment, the ``Host`` and/or ``X-Odoo-Database`` request headers might be
|
|
required. The ``Host`` header is required by HTTP/1.1 and is needed on servers where Odoo is
|
|
installed next to other web applications, so that a web-server/reverse-proxy is able to route the
|
|
request to the Odoo server. The ``X-Odoo-Database`` header is required when a single Odoo server
|
|
hosts multiple databases and the :ref:`dbfilter` wasn't configured to use the ``Host`` header.
|
|
|
|
Most HTTP client libraries automatically set the ``Host`` header using the connection URL.
|
|
|
|
.. _reference/external_api/transaction:
|
|
|
|
Transaction
|
|
===========
|
|
|
|
All calls to the JSON-2 endpoint run in their own SQL transaction. The transaction is committed in
|
|
case of success and is discarded in case of error. Using the JSON-2 API, it is not possible to chain
|
|
multiple calls inside a single transaction. It means that one must be cautious when making multiple
|
|
consecutive calls, as the database might be modified by other concurrent transactions. This is
|
|
especially dangerous when performing operations related to reservations, payments, and such.
|
|
|
|
The solution is to always call a single method that performs all the related operations in a single
|
|
transaction. This way, the data is guaranteed to stay consistent: either everything is done
|
|
(success, commit), or nothing is done (error, rollback).
|
|
|
|
In the ORM, the ``search_read`` method is an example of a single method that performs multiple
|
|
operations (``search`` then ``read``) in a single transaction. If a concurrent request removes one
|
|
of the records ``search`` retrieves, then there is a risk that subsequent calls to ``read`` fail for
|
|
a missing record error. Such a problem cannot occur in ``search_read``, as the system guarantees
|
|
proper isolation between transactions.
|
|
|
|
In business models, those methods are often prefixed by ``action_``, such as
|
|
``sale.order``'s ``action_confirm`` method, which verifies that a sales order is valid before
|
|
confirming it.
|
|
|
|
When no method exists for a set of related operations, a new one can be created in a dedicated
|
|
module.
|
|
|
|
.. seealso::
|
|
- :doc:`Tutorial to create a module <../tutorials/server_framework_101>`
|
|
- PostgreSQL - Transaction Isolation - `Repeatable Read
|
|
<https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ>`_
|
|
|
|
.. _reference/external_api/code_example:
|
|
|
|
Code Example
|
|
============
|
|
|
|
The following examples showcase how to execute two of the :ref:`common ORM methods
|
|
<reference/orm/models/crud>` on a dummy database ``mycompany`` hosted on the dummy website
|
|
``https://mycompany.example.com``. Its :ref:`dynamic documentation
|
|
<reference/external_api/dynamic_doc>` would be available at https://mycompany.example.com/doc.
|
|
|
|
.. tabs::
|
|
|
|
.. code-tab:: python
|
|
|
|
import requests
|
|
|
|
BASE_URL = "https://mycompany.example.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_search = requests.post(
|
|
f"{BASE_URL}/res.partner/search",
|
|
headers=headers,
|
|
json={
|
|
"context": {"lang": "en_US"},
|
|
"domain": [
|
|
("name", "ilike", "%deco%"),
|
|
("is_company", "=", True),
|
|
],
|
|
},
|
|
)
|
|
res_search.raise_for_status()
|
|
ids = res_search.json()
|
|
|
|
res_read = requests.post(
|
|
f"{BASE_URL}/res.partner/read",
|
|
headers=headers,
|
|
json={
|
|
"ids": ids,
|
|
"context": {"lang": "en_US"},
|
|
"fields": ["name"],
|
|
}
|
|
)
|
|
res_read.raise_for_status()
|
|
names = res_read.json()
|
|
print(names)
|
|
|
|
.. code-tab:: javascript
|
|
|
|
(async () => {
|
|
const BASE_URL = "https://mycompany.example.com/json/2";
|
|
const API_KEY = ; // get it from a secure location
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
"Authorization": "bearer " + API_KEY,
|
|
"X-Odoo-Database": DATABASE,
|
|
}
|
|
|
|
const reqSearch = {
|
|
method: "POST",
|
|
headers: headers,
|
|
body: {
|
|
context: {lang: "en_US"},
|
|
domain: [
|
|
["name", "ilike", "%deco%"],
|
|
["is_company", "=", true],
|
|
],
|
|
},
|
|
};
|
|
const resSearch = await fetch(BASE_URL + "/res.partner/search_read", reqSearch);
|
|
if (!response.ok) throw new Error(resSearch.json());
|
|
const ids = await resSearch.json();
|
|
|
|
const reqRead = {
|
|
method: "POST",
|
|
headers: headers,
|
|
body: {
|
|
ids: ids,
|
|
context: {lang: "en_US"},
|
|
fields: ["name"],
|
|
},
|
|
};
|
|
const resRead = await fetch(BASE_URL + "/res.partner/search_read", reqRead);
|
|
if (!response.ok) throw new Error(resRead.json());
|
|
const names = await resRead.json();
|
|
console.log(names);
|
|
})();
|
|
|
|
.. code-tab:: bash
|
|
|
|
set -eu
|
|
|
|
DATABASE=mycompany
|
|
BASE_URL=https://$DATABASE.odoo.com/json/2
|
|
API_KEY=
|
|
|
|
ids=$(curl $BASE_URL/res.partner/search \
|
|
-X POST \
|
|
--oauth2-bearer $API_KEY \
|
|
-H "X-Odoo-Database: $DATABASE" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"context": {"lang": "en_US"}, "domain": [["name", "ilike", "%deco%"], ["is_company", "=", true]]}' \
|
|
--silent \
|
|
--fail
|
|
)
|
|
curl $BASE_URL/res.partner/read \
|
|
-X POST \
|
|
--oauth2-bearer $API_KEY \
|
|
-H "X-Odoo-Database: $DATABASE" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"ids\": $ids, \"context\": {\"lang\": \"en_US\"}, \"fields\": [\"name\"]}" \
|
|
--silent \
|
|
--fail-with-body
|
|
|
|
The above example is equivalent to running::
|
|
|
|
Model = self.env["res.partner"].with_context({"lang": "en_US"})
|
|
records = Model.search([("name", "ilike", "%deco%"), ("is_company", "=", True)])
|
|
return json.dumps(records.ids)
|
|
|
|
Then, in a new transaction::
|
|
|
|
records = self.env["res.partner"].with_context({"lang": "en_US"}).browse(ids)
|
|
names = records.read(["name"])
|
|
return json.dumps(names)
|
|
|
|
.. _reference/external_api/dynamic_doc:
|
|
|
|
Dynamic Documentation
|
|
=====================
|
|
|
|
Under construction
|
|
|
|
.. _reference/external_api/migration:
|
|
|
|
Migrating from XML-RPC / JSON-RPC
|
|
=================================
|
|
|
|
Both the XML-RPC and JSON-RPC APIs at endpoints ``/xmlrpc``, ``/xmlrpc/2`` and ``/jsonrpc`` are
|
|
scheduled for removal in Odoo 20 (fall 2026). Both RPC APIs expose the three same services: common,
|
|
db (database) and object. All three services are deprecated.
|
|
|
|
.. note::
|
|
|
|
The other controllers ``@route(type='jsonrpc')`` (known until Odoo 18 as ``type='json'``) are not
|
|
subject to this deprecation notice.
|
|
|
|
Common service
|
|
--------------
|
|
|
|
The common service defines 3 fonctions:
|
|
|
|
1. ``version()``
|
|
2. ``login(db, login, password)``
|
|
3. ``authenticate(db, login, password, user_agent_env)``
|
|
|
|
The version function is replaced by the ``/web/version`` endpoint.
|
|
|
|
.. code:: http
|
|
|
|
GET /web/version HTTP/1.1
|
|
|
|
.. code:: http
|
|
|
|
HTTP/1.1 200 OK
|
|
Content-Type: application/json
|
|
|
|
{"version_info": [19, 0, 0, "final", 0, ""], "version": "19.0"}
|
|
|
|
The two ``login`` and ``authenticate`` functions return the user ID corresponding to the user after
|
|
a successful login. The user ID and password are necessary for subsequent RPC calls to the *object*
|
|
service. The JSON-2 API uses a different authentication scheme where neither the user ID nor the
|
|
password are used. It is still possible to retrieve the user's own ID by sending a JSON-2 request to
|
|
``res.users/context_get`` with no ID (the current user is extracted from the API key).
|
|
|
|
Database service
|
|
----------------
|
|
|
|
.. seealso::
|
|
:ref:`db_manager_security`
|
|
|
|
The db service defines 13 fonctions:
|
|
|
|
#. ``create_database(master_pwd, db_name, demo, lang, user_password, login, country_code, phone)``
|
|
#. ``duplicate_database(master_pwd, db_original_name, db_name, neutralize_database)``
|
|
#. ``drop(master_pwd, db_name)``
|
|
#. ``dump(master_pwd, db_name, format)``
|
|
#. ``restore(master_pwd, db_name, data, copy)``
|
|
#. ``change_admin_password(master_pwd, new_password)``
|
|
#. ``rename(master_pwd, old_name, new_name)``
|
|
#. ``migrate_databases(master_pwd, databases)``
|
|
#. ``db_exist(db_name)``
|
|
#. ``list()``
|
|
#. ``list_lang()``
|
|
#. ``list_countries(master_pwd)``
|
|
#. ``server_version()``
|
|
|
|
Many of those function are accessible via the ``/web/database`` controllers. Those controllers
|
|
work hand-in-hand with the HTML form at ``/web/database/manager`` and are accessible via HTTP.
|
|
|
|
The following controllers use the verb ``POST`` and content-type
|
|
``application/x-www-form-urlencoded``.
|
|
|
|
#. ``/web/database/create`` takes inputs ``master_pwd``, ``name``, ``login``, ``password``,
|
|
``demo``, ``lang``, and ``phone``.
|
|
#. ``/web/database/duplicate`` takes inputs ``master_pwd``, ``name``, ``new_name``, and
|
|
``neutralize_database`` (not neutralized by default).
|
|
#. ``/web/database/drop`` takes inputs ``master_pwd`` and ``name``.
|
|
#. ``/web/database/backup`` takes inputs ``master_pwd``, ``name``, and ``backup_format`` (zip by
|
|
default), and returns the backup in the http response.
|
|
#. ``/web/database/change_password`` takes inputs ``master_pwd`` and ``master_pwd_new``.
|
|
|
|
The following controller uses the verb ``POST`` and content-type ``multipart/form-data``.
|
|
|
|
* ``/web/database/restore`` takes inputs ``master_pwd``, ``name``, ``copy`` (not copied by
|
|
default) and ``neutralize`` (not neutralized by default), it takes a file input ``backup_file``.
|
|
|
|
The following controller uses the verb ``POST`` and content-type ``application/json-rpc``.
|
|
|
|
* ``/web/database/list`` takes an empty JSON object as input, and returns the database list under
|
|
the JSON response's ``result`` entry.
|
|
|
|
The remaining function are: ``server_version``, which exists under ``/web/version``, ``list_lang``,
|
|
and ``list_countries``, which exist via JSON-2 on the ``res.lang`` and ``res.country`` models, and
|
|
``migrate_databases``, which as non-programmable API at the moment.
|
|
|
|
Object service
|
|
--------------
|
|
|
|
The object service defines 2 fonctions:
|
|
|
|
#. ``execute(db, uid, passwd, model, method, *args)``
|
|
#. ``execute_kw(db, uid, passwd, model, method, args, kw={})``
|
|
|
|
They both give for access to all public model methods, including the generic ORM ones.
|
|
|
|
Both functions are stateless. It means that the database, user ID and user password are to be
|
|
provided for each call. The model, method and arguments must be provided, too. The ``execute``
|
|
function takes as many extra positional arguments as necessary. The ``execute_kw`` function takes an
|
|
``args`` list of positional arguments and an optional ``kw`` dict of keyword arguments.
|
|
|
|
The records IDs are extracted from the first ``args``. When the called method is decorated with
|
|
``@api.model``, no record ID is extracted, and ``args`` is left as-is. It is only possible to give a
|
|
context with ``execute_kw``, as it is extracted from the keyword argument named ``context``.
|
|
|
|
.. example::
|
|
To run the following:
|
|
|
|
.. code:: python
|
|
|
|
(env['res.partner']
|
|
.with_user(2) # admin
|
|
.with_context(lang='en_US')
|
|
.browse([1, 2, 3])
|
|
.read(['name'], load=None)
|
|
)
|
|
|
|
Using XML-RPC (JSON-RPC would be similar):
|
|
|
|
.. code:: python
|
|
|
|
from xmlrpc.client import ServerProxy
|
|
object = ServerProxy(...)
|
|
ids = [1, 2, 3]
|
|
fields = ['name']
|
|
load = None
|
|
|
|
object.execute("database", 2, "admin", "res.partner", "read", ids, fields, load)
|
|
object.execute("database", 2, "admin", "res.partner", "search", [
|
|
ids,
|
|
fields,
|
|
], {
|
|
"context": {"lang": "en_US"},
|
|
"load": load,
|
|
})
|
|
|
|
The JSON-2 API replaces the object service with a few differences. The database must only be
|
|
provided (via the ``X-Odoo-Database`` HTTP header) on systems where there are multiple databases
|
|
available for a same domain. The login/password authentication scheme is replaced by an API key (via
|
|
the ``Authorization: bearer`` HTTP header). The ``model`` and ``method`` are placed in the URL. The
|
|
request body is a JSON object with all the methods arguments, plus ``ids`` and ``context``. All
|
|
the arguments are named; there is no way in JSON-2 to call a function with positional arguments.
|
|
|
|
.. example::
|
|
Using JSON-2:
|
|
|
|
.. code:: python
|
|
|
|
import requests
|
|
|
|
DATABSE = ...
|
|
DOMAIN = ...
|
|
API_KEY = "6578616d706c65206a736f6e20617069206b6579"
|
|
|
|
requests.post(
|
|
f"https://{DOMAIN}/json/2/res.partner/read",
|
|
headers={
|
|
# "X-Odoo-Database": DATABASE, # only when DOMAIN isn't enough
|
|
"Authorization": f"bearer {API_KEY}",
|
|
},
|
|
json={
|
|
"ids": [1, 2, 3],
|
|
"context": {"lang": "en_US"},
|
|
"fields": ["name"],
|
|
"load": None,
|
|
},
|
|
).json()
|