Odoo 16 introduced a first-class REST API as an alternative to XML-RPC and JSON-RPC. The REST API uses standard HTTP verbs, returns JSON, and supports API-key authentication - making it a better fit for modern integrations. This guide covers the full CRUD surface.
Authentication#
API keys#
Generate an API key in Settings → Technical → API Keys → New. Give it a name, set an expiry if required by your security policy, and save. The key is shown once - copy it immediately.
Include the key in every request as a Bearer token:
curl -X GET "https://your-odoo.com/api/res.partner" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"API keys are tied to the user who generated them and inherit that user's access rights. A key generated by a demo user cannot read records that demo user cannot read.
Session-based authentication (alternative)#
The REST API also accepts the session cookie from a browser login, which is useful during development but should not be used for production integrations. Stick to API keys for integrations.
Base URL structure#
All REST endpoints follow the pattern:
https://your-odoo.com/api/{model_technical_name}Where {model_technical_name} is the dotted model name: res.partner, sale.order, product.template, etc.
For Odoo.sh, the URL is https://{branch}-{database}.{project}.odoo.sh. For Community or self-hosted, it's your instance domain.
Listing records (GET)#
GET /api/res.partnerReturns a JSON array of all accessible partners. By default, returns all fields from the model's _rec_name and a handful of base fields.
Filtering with domain#
Pass a JSON-encoded Odoo domain as the domain query parameter:
GET /api/res.partner?domain=[["is_company","=",true],["country_id.code","=","CA"]]URL-encode the domain string. In Python:
import urllib.parse, requests
domain = [["is_company","=",True],["country_id.code","=","CA"]]
params = {"domain": str(domain)}
r = requests.get(
"https://your-odoo.com/api/res.partner",
params=params,
headers={"Authorization": "Bearer YOUR_API_KEY"}
)Selecting fields#
Use fields to restrict the response to specific fields:
GET /api/res.partner?fields=["name","email","phone","country_id"]Relational fields (Many2one) return {"id": 44, "display_name": "Canada"} by default.
Pagination#
Use limit and offset:
GET /api/res.partner?limit=50&offset=0 # page 1
GET /api/res.partner?limit=50&offset=50 # page 2The default limit is 1000. There is no cursor-based pagination - offset is the only mechanism. For large exports, sort by id and paginate by [["id",">",last_id]] domain for better performance.
Ordering#
GET /api/res.partner?order=name+asc
GET /api/sale.order?order=date_order+desc,id+ascReading a single record (GET by ID)#
GET /api/res.partner/44Returns the full field set for record 44. Add ?fields=[...] to restrict.
Creating a record (POST)#
POST /api/res.partner
Content-Type: application/json
{
"name": "Acme Corp",
"is_company": true,
"email": "info@acme.example",
"country_id": 38
}Many2one fields accept the integer ID directly. One2many and Many2many fields are not supported in POST/PATCH - use separate calls to create related records. The response is the created record's id.
Updating a record (PATCH)#
PATCH /api/res.partner/44
Content-Type: application/json
{
"phone": "+1-800-555-0100",
"category_id": [{"id": 5}]
}Many2many fields use Odoo's command syntax embedded in the JSON. To set tags:
{
"category_id": [{"id": 5}, {"id": 8}]
}This replaces the current set with IDs 5 and 8. To add without replacing, use the full command notation (see JSON-RPC documentation) - the REST API exposes a simplified syntax that replaces, not appends.
Deleting a record (DELETE)#
DELETE /api/res.partner/44Returns true on success. The user must have delete rights on the model. Odoo will raise an error if the record has constraints that prevent deletion (e.g., a partner with open invoices).
Calling custom methods#
To call a model method that is not a standard CRUD operation, use:
POST /api/sale.order/1/action_confirm
Content-Type: application/json
{}This calls SaleOrder.action_confirm() on record ID 1. The body can pass keyword arguments:
POST /api/stock.picking/12/button_validate
Content-Type: application/json
{}Not all methods are exposed via REST. Methods decorated with @api.model or @api.model_create_multi can be called as:
POST /api/res.partner/call/create_from_external
Content-Type: application/json
{"name": "Jane Doe", "email": "jane@example.com"}Use the /call/{method_name} path for @api.model methods.
Error handling#
Errors return HTTP 4xx/5xx with a JSON body:
{
"code": 403,
"message": "Access Denied",
"data": {"message": "You don't have access to perform this operation."}
}Common errors:
- 401 - invalid or expired API key
- 403 - access rights violation (check the user's model access)
- 404 - record not found, or the model doesn't exist
- 422 - validation error (required field missing, type mismatch)
- 500 - server error (check the Odoo log for details)
Rate limiting and connection pooling#
Odoo does not apply rate limiting at the REST layer. For high-volume integrations:
- Use connection pooling on the HTTP client side (keep-alive)
- Batch reads with
domain+limitloops rather than individual GET-by-ID calls - Write in batches using a loop over your dataset; REST does not support bulk create in a single request
Comparing REST vs JSON-RPC vs XML-RPC#
| Criterion | REST API | JSON-RPC | XML-RPC |
|---|---|---|---|
| Auth | API key (Bearer) | Session / API key | Username + password |
| Format | JSON (standard HTTP verbs) | JSON over POST only | XML |
| Available since | Odoo 16 | All versions | All versions |
| Relational field writes | Simplified | Full command list | Full command list |
| Custom method calls | /call/{method} | execute_kw | execute_kw |
| Best for | New integrations | Existing code, complex writes | Legacy systems |
For new integrations starting from Odoo 16+, REST is the recommended approach. For complex Many2many manipulations with add/remove semantics, JSON-RPC gives more control.
Query your Odoo instance schema and method signatures with ERPeek - ask "what fields does sale.order expose via the REST API" or "show me the action methods on stock.picking" without reading source files.

