外部 JSON-2 API¶
19.0 新版功能.
Odoo 通常通过模块在内部进行扩展,但它的许多功能和全部数据也可以在外部访问,用于分析或与各种其他软件集成。型号 API 的一部分可以通过 /json/2 端点经 HTTP 轻松访问。
小技巧
实际可用的模型、字段和方法因每个数据库而异,可以在其 /doc 页面中查看。
注解
通过外部 API 访问数据仅在 Custom Odoo 定价计划中可用。外部 API 访问在 One App Free 或 Standard 计划中不可用。更多信息请访问 Odoo 定价页面 或联系您的客户成功经理。
API¶
请求¶
向 /json/2/<model>/<method> URL 发送一个 JSON 对象。
HTTP 请求头
- Host
必填,服务器的主机名。
- Autorization
必填,
bearer后跟 API 密钥。- Content-Type
必填,
application/json,建议指定字符集。- X-Odoo-Database
可选,要连接的数据库名称。
- User-Agent
建议填写,您的软件的名称。
URL 路径
- model
必填,模型的技术名称。
- method
必填,要执行的方法。
请求体 JSON 对象
- ids
要在其上执行该方法的记录 ID 数组。调用由
@api.model装饰的方法时为空或省略。- context
可选,一个包含附加值的对象。例如
{"lang": "en_US"}。- param
按需任意多个,即该方法的参数。
示例
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"]
}
响应¶
成功 时,返回 200 状态,响应体中包含被调用方法的 JSON 序列化返回值。
示例
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
[
{"id": 25, "name": "Deco Addict"}
]
出错 时,返回 4xx/ 5xx 状态,响应体中包含 JSON 序列化的错误对象。
- name
所发生 Python 异常的完全限定名称。
- message
异常消息,通常与
arguments[0]相同。- arguments
所有异常参数。
- context
请求所使用的上下文。
- debug
异常 traceback,用于调试。
示例
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"
}
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
配置¶
API 密钥¶
手动生成密钥¶
必须在 Authorization 请求标头中以 bearer 令牌的形式设置 API 密钥。
通过 为用户创建新的 API 密钥。
|
|
|
创建新的 API 密钥需要同时提供描述和有效期。描述可用于识别密钥,并在日后判断该密钥是否仍在使用、还是应当删除。有效期决定密钥的生存时间,过期后密钥即失效。对于交互式用途,建议设置较短的有效期(通常为一天)。出于安全原因,无法创建有效期超过三个月的密钥。这意味着长期使用的密钥必须至少每三个月轮换一次。
生成密钥 按钮会创建一个强度较高的 160 位随机密钥。密钥值仅在创建时显示一次,之后无法再获取。请立即复制该密钥并妥善保管。如果密钥已泄露或丢失,请立即将其删除并生成新密钥。
关于 API 密钥管理的更多指导,请参阅 OWASP 机密信息管理速查表。
以编程方式管理密钥¶
Odoo 提供可通过编程方式生成和撤销 API 密钥的 RPC 方法。
先决条件¶
默认情况下,以编程方式管理 API 密钥仅限具有 设置 管理访问权限的用户使用。
要让其他用户能够以编程方式管理 API 密钥,请导航至 ,并将参数 base.enable_programmatic_api_keys 设置为 True。
生成密钥¶
可以使用 res.users.apikeys.generate() 生成 API 密钥。
该方法接受以下参数:
key(字符串):一个现有的有效 API 密钥scope(字符串或null):分配给新密钥的作用域name(字符串):便于人阅读的标签expiration_date(字符串):以 ISO 8601 格式表示的过期日期(例如"2026-05-19")
作用域限定了 API 密钥可被使用的位置。
rpc 作用域是通用作用域,用于通过带 auth='bearer' 的控制器进行 RPC 访问。
新密钥通常应使用与现有密钥相同的作用域。不过,在需要时可以从无作用域密钥创建有作用域的密钥。无作用域密钥的作用域值为 null (在 Python 中为 None)。
过期日期会根据分配给用户的角色所允许的 API 密钥最长有效期进行校验。
对于高权限或对外暴露的集成,应使用较短的过期期限;只有在严格控制的内网系统中才使用较长的期限。
示例
import requests
API_KEY = ... # get it from a secure location
res_apikey = requests.post(
f"https://mycompany.example.com/json/2/res.users.apikeys/generate",
headers={"Authorization": f"bearer {API_KEY}"},
json={
"key": API_KEY,
"scope": None,
"name": "Some service",
"expiration_date": "2026-05-19",
},
)
res_apikey.raise_for_status()
new_apikey = res_apikey.json()
# store the new key securely
(async () => {
const API_KEY = ; // get it from a secure location
const headers = {
"Content-Type": "application/json",
"Authorization": "bearer " + API_KEY,
}
const reqSearch = {
method: "POST",
headers: headers,
body: JSON.stringify({
"key": API_KEY,
"scope": null,
"name": "Some service",
"expiration_date": "2026-05-19",
}),
};
const resSearch = await fetch("https://mycompany.example.com/json/2/res.users.apikeys/generate", reqSearch);
if (!resSearch.ok) throw new Error(resSearch.json());
const new_apikey = await resSearch.json();
// store the new key securely
})();
set -eu
API_KEY=
curl https://mycompany.example.com/json/2/res.users.apikeys/generate \
-X POST \
--oauth2-bearer $API_KEY \
-H "Content-Type: application/json" \
-d '{
"key": "'"$API_KEY"'",
"scope": null,
"name": "Some service",
"expiration_date": "2026-05-19"
}' \
--silent \
--fail
# store the new key securely
该方法以字符串形式返回新生成的 API 密钥,此密钥之后无法再获取。
默认情况下,每个用户最多可以以编程方式生成 10 个 API 密钥。此上限可通过系统参数 base.programmatic_api_keys_limit 进行配置。
尝试超出配置的上限会失败,并返回 HTTP 422 Unprocessable Content 状态码。
撤销密钥¶
可以使用 res.users.apikeys.revoke() 撤销 API 密钥。
该方法接受以下参数:
key(字符串):要撤销的 API 密钥
示例
import requests
API_KEY = ... # get it from a secure location
res_apikey = requests.post(
f"https://mycompany.example.com/json/2/res.users.apikeys/revoke",
headers={"Authorization": f"bearer {API_KEY}"},
json={
"key": API_KEY,
},
)
res_apikey.raise_for_status()
(async () => {
const API_KEY = ; // get it from a secure location
const headers = {
"Content-Type": "application/json",
"Authorization": "bearer " + API_KEY,
}
const reqSearch = {
method: "POST",
headers: headers,
body: JSON.stringify({
"key": API_KEY,
}),
};
const resSearch = await fetch("https://mycompany.example.com/json/2/res.users.apikeys/revoke", reqSearch);
if (!resSearch.ok) throw new Error(resSearch.json());
})();
set -eu
API_KEY=
curl https://mycompany.example.com/json/2/res.users.apikeys/revoke \
-X POST \
--oauth2-bearer $API_KEY \
-H "Content-Type: application/json" \
-d "{\"key\": \"$API_KEY\"}" \
--silent \
--fail
如果密钥有效,它会立即被撤销,并且无法再用于后续请求。否则,请求会失败并返回 HTTP 403 Forbidden 状态码。
请注意,被撤销的密钥与 Authorization 标头中提供的密钥无需相同。轮换期间的良好做法是使用新密钥进行身份验证,以撤销旧密钥。
密钥轮换最佳实践¶
定期轮换 API 密钥可降低密钥泄露后被未授权访问的风险。
轮换 API 密钥时:
在撤销旧密钥之前先生成新密钥。
在部署之前安全地存储新密钥。
确认所有服务都在使用新密钥。
仅在切换完成后才撤销旧密钥。
使用符合系统安全要求的过期日期。
最好为每个服务或集成使用专用的 API 密钥,以简化审计和撤销操作。
访问权限¶
JSON-2 API 使用 Odoo 标准的 安全模型。所有操作都会根据用户的访问权限、记录规则和字段访问权限进行校验。
对于 交互式用途,例如探索 API 或运行一次性脚本,使用 个人账户 没有问题。
对于 扩展的自动化用途,例如与另一个软件集成,建议创建并使用 专用机器人用户。使用专用机器人用户有以下几项好处:
可以只向机器人授予最低必需的权限,从而限制 API 密钥泄露时的影响。
可以将密码设置为空,以禁用登录名/密码身份验证,降低账户被入侵的可能性。
日志访问记录 使用机器人账户。不会冒用任何用户的身份。
数据库¶
根据部署方式的不同,可能需要 Host 和/或 X-Odoo-Database 请求标头。Host 标头是 HTTP/1.1 的要求,在 Odoo 与其他 Web 应用一同安装的服务器上需要它,以便 Web 服务器/反向代理能够将请求路由到 Odoo 服务器。当一台 Odoo 服务器托管多个数据库,且 数据库过滤器 未配置为使用 Host 标头时,则需要 X-Odoo-Database 标头。
大多数 HTTP 客户端库会根据连接 URL 自动设置 Host 标头。
事务¶
对 JSON-2 端点的所有调用都在各自的 SQL 事务中运行。调用成功时提交事务,出错时则丢弃事务。使用 JSON-2 API 时,无法在单个事务中串联多个调用。这意味着在进行多次连续调用时必须谨慎,因为数据库可能会被其他并发事务修改。在执行与预留、付款等相关的操作时,这种情况尤其危险。
解决方案是始终调用单个方法,在单个事务中执行所有相关操作。这样可以保证数据保持一致:要么全部完成(成功,提交),要么什么都不做(出错,回滚)。
在 ORM 中,search_read 方法就是一个在单个事务中执行多个操作(先 search 后 read)的单一方法示例。如果并发的请求删除了 search 检索到的某条记录,那么后续对 read 的调用就可能因记录缺失错误而失败。而在 search_read 中不会出现这种问题,因为系统保证了事务之间的正确隔离。
在业务模型中,这类方法通常以 action_ 为前缀,例如 sale.order 的 action_confirm 方法,它会在确认销售订单之前先校验订单是否有效。
如果某一组相关操作还没有对应的方法,可以在专用模块中新建一个方法。
代码示例¶
以下示例展示如何在托管于虚拟网站 https://mycompany.example.com 的虚拟数据库 mycompany 上执行两个 常用 ORM 方法。其 动态文档 可在 https://mycompany.example.com/doc 获取。
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)
(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: JSON.stringify({
context: {lang: "en_US"},
domain: [
["name", "ilike", "%deco%"],
["is_company", "=", true],
],
}),
};
const resSearch = await fetch(BASE_URL + "/res.partner/search_read", reqSearch);
if (!resSearch.ok) throw new Error(resSearch.json());
const ids = await resSearch.json();
const reqRead = {
method: "POST",
headers: headers,
body: JSON.stringify({
ids: ids,
context: {lang: "en_US"},
fields: ["name"],
}),
};
const resRead = await fetch(BASE_URL + "/res.partner/search_read", reqRead);
if (!resRead.ok) throw new Error(resRead.json());
const names = await resRead.json();
console.log(names);
})();
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
上面的示例等价于运行以下代码:
Model = self.env["res.partner"].with_context({"lang": "en_US"})
records = Model.search([("name", "ilike", "%deco%"), ("is_company", "=", True)])
return json.dumps(records.ids)
然后,在一个新事务中:
records = self.env["res.partner"].with_context({"lang": "en_US"}).browse(ids)
names = records.read(["name"])
return json.dumps(names)
动态文档¶
正在建设中
从 XML-RPC / JSON-RPC 迁移¶
位于 /xmlrpc、/xmlrpc/2 和 /jsonrpc 端点的 XML-RPC 与 JSON-RPC API 均已弃用。这两个 RPC API 都提供相同的三个服务:common、db(数据库)和 object。db 服务已在 Odoo 20(2026 年秋季)和 Online 19.1(2025 年冬季)中移除。common 和 object 服务计划在 Odoo 22(2028 年秋季)和 Online 21.1(2027 年冬季)中移除。
注解
其他 @route(type='jsonrpc') 控制器(在 Odoo 18 之前称为 type='json')不受此弃用通知的影响。
common 服务¶
common 服务定义了 3 个函数:
version()login(db, login, password)authenticate(db, login, password, user_agent_env)
version 函数已被 /web/version 端点取代。
GET /web/version HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{"version_info": [19, 0, 0, "final", 0, ""], "version": "19.0"}
login 和 authenticate 这两个函数在登录成功后返回该用户对应的用户 ID。用户 ID 和密码是后续调用 object 服务的 RPC 所必需的。JSON-2 API 使用不同的身份验证方案,既不使用用户 ID 也不使用密码。仍然可以通过向 res.users/context_get 发送不带 ID 的 JSON-2 请求来获取用户自己的 ID(当前用户从 API 密钥中提取)。
db 服务¶
更多内容
db 服务定义了 13 个函数:
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()
上述函数中有许多可通过 /web/database 控制器访问。这些控制器与位于 /web/database/manager 的 HTML 表单配合使用,并可通过 HTTP 访问。
以下控制器使用动词 POST 和内容类型 application/x-www-form-urlencoded。
/web/database/create接受master_pwd、name、login、password、demo、lang和phone作为输入。/web/database/duplicate接受master_pwd、name、new_name和neutralize_database作为输入(默认不进行中和)。/web/database/drop接受master_pwd和name作为输入。/web/database/backup接受master_pwd、name和backup_format(默认为 zip)作为输入,并在 HTTP 响应中返回备份文件。/web/database/change_password接受master_pwd和master_pwd_new作为输入。
以下控制器使用动词 POST 和内容类型 multipart/form-data。
/web/database/restore接受master_pwd、name、copy(默认不复制)和neutralize(默认不中和)作为输入,还接受一个文件输入backup_file。
以下控制器使用动词 POST 和内容类型 application/json-rpc。
/web/database/list接受一个空 JSON 对象作为输入,并在 JSON 响应的result条目下返回数据库列表。
其余函数包括:server_version,它存在于 /web/version 下;list_lang 和 list_countries,它们通过 JSON-2 在 res.lang 和 res.country 模型上提供;以及 migrate_databases,它目前还没有可编程 API。
object 服务¶
object 服务定义了 2 个函数:
execute(db, uid, passwd, model, method, *args)execute_kw(db, uid, passwd, model, method, args, kw={})
两者都可用于访问所有公共模型方法,包括通用的 ORM 方法。
这两个函数都是无状态的。这意味着每次调用都需要提供数据库、用户 ID 和用户密码。此外还必须提供模型、方法和参数。execute 函数接受所需数量的额外位置参数。execute_kw 函数接受由位置参数组成的 args 列表以及可选的关键字参数字典 kw。
记录 ID 从第一个 args 中提取。当被调用的方法带有 @api.model 装饰器时,不会提取记录 ID,args 保持原样。只有使用 execute_kw 才能传入上下文,因为它是从名为 context 的关键字参数中提取的。
示例
要运行以下代码:
(env['res.partner']
.with_user(2) # admin
.with_context(lang='en_US')
.browse([1, 2, 3])
.read(['name'], load=None)
)
使用 XML-RPC(JSON-RPC 类似):
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,
})
JSON-2 API 取代了 object 服务,但有一些差异。只有在同一域名下存在多个可用数据库的系统中,才需要(通过 X-Odoo-Database HTTP 标头)提供数据库。登录名/密码身份验证方案被 API 密钥取代(通过 Authorization: bearer HTTP 标头)。model 和 method 放在 URL 中。请求体是一个 JSON 对象,包含方法的所有参数以及 ids 和 context。所有参数都是具名的;在 JSON-2 中无法使用位置参数调用函数。
示例
使用 JSON-2:
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()