All posts
Developer11 min read

Using the Odoo REST API: endpoints, authentication, and CRUD operations

A practical guide to Odoo 16+ REST API - how to authenticate with API keys, call model endpoints, filter records, and handle pagination without XML-RPC.

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:

bash
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)#

bash
GET /api/res.partner

Returns 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:

bash
GET /api/res.partner?domain=[["is_company","=",true],["country_id.code","=","CA"]]

URL-encode the domain string. In Python:

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:

bash
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:

bash
GET /api/res.partner?limit=50&offset=0   # page 1
GET /api/res.partner?limit=50&offset=50  # page 2

The 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#

bash
GET /api/res.partner?order=name+asc
GET /api/sale.order?order=date_order+desc,id+asc

Reading a single record (GET by ID)#

bash
GET /api/res.partner/44

Returns the full field set for record 44. Add ?fields=[...] to restrict.

Creating a record (POST)#

bash
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)#

bash
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:

json
{
  "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)#

bash
DELETE /api/res.partner/44

Returns 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:

bash
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:

bash
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:

bash
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:

json
{
  "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 + limit loops 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#

CriterionREST APIJSON-RPCXML-RPC
AuthAPI key (Bearer)Session / API keyUsername + password
FormatJSON (standard HTTP verbs)JSON over POST onlyXML
Available sinceOdoo 16All versionsAll versions
Relational field writesSimplifiedFull command listFull command list
Custom method calls/call/{method}execute_kwexecute_kw
Best forNew integrationsExisting code, complex writesLegacy 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.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free