Why Build Custom REST APIs in Odoo?
Odoo ships with XML-RPC and JSON-RPC APIs, but they expose Odoo's internal data model directly. When you need a clean, well-documented API for mobile apps, third-party integrations, or frontend SPAs, custom REST endpoints give you full control over the request format, response structure, authentication, and business logic. You define the contract, not Odoo's ORM.
Odoo Controller Basics
Odoo controllers are Python classes that handle HTTP requests. They work like Flask or Django views:
from odoo import http
from odoo.http import request, Response
import json
class MyAPI(http.Controller):
@http.route('/api/v1/products', type='http', auth='none',
methods=['GET'], csrf=False)
def get_products(self, **kwargs):
# Your logic here
products = request.env['product.product'].sudo().search_read(
[('sale_ok', '=', True)],
fields=['name', 'list_price', 'default_code', 'qty_available'],
limit=50
)
return Response(
json.dumps({'status': 'ok', 'data': products}),
content_type='application/json',
status=200
)Step 1: Create the Module Structure
my_api/
__init__.py
__manifest__.py
controllers/
__init__.py
api.py__manifest__.py
{
'name': 'My Custom API',
'version': '17.0.1.0.0',
'depends': ['base', 'product', 'sale'],
'data': [],
'installable': True,
}Step 2: Implement Authentication
There are several authentication approaches for REST APIs in Odoo:
API Key Authentication
def _authenticate(self):
api_key = request.httprequest.headers.get('X-API-Key')
if not api_key:
return None, self._error_response('Missing API key', 401)
user = request.env['res.users'].sudo().search([
('api_key_ids.key', '=', api_key)
], limit=1)
if not user:
return None, self._error_response('Invalid API key', 401)
return user, NoneBearer Token (JWT)
import jwt
def _authenticate_jwt(self):
auth_header = request.httprequest.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return None, self._error_response('Missing token', 401)
token = auth_header[7:]
try:
payload = jwt.decode(token, PUBLIC_KEY, algorithms=['RS256'])
user = request.env['res.users'].sudo().browse(payload['uid'])
return user, None
except jwt.ExpiredSignatureError:
return None, self._error_response('Token expired', 401)
except jwt.InvalidTokenError:
return None, self._error_response('Invalid token', 401)Step 3: Build CRUD Endpoints
class ProductAPI(http.Controller):
@http.route('/api/v1/products', type='http', auth='none',
methods=['GET'], csrf=False)
def list_products(self, page=1, limit=20, **kwargs):
user, error = self._authenticate()
if error:
return error
offset = (int(page) - 1) * int(limit)
domain = [('sale_ok', '=', True)]
total = request.env['product.product'].with_user(user).search_count(domain)
products = request.env['product.product'].with_user(user).search_read(
domain,
fields=['name', 'list_price', 'default_code'],
limit=int(limit),
offset=offset,
order='name ASC'
)
return self._json_response({
'data': products,
'pagination': {
'page': int(page),
'limit': int(limit),
'total': total,
'pages': (total + int(limit) - 1) // int(limit)
}
})
@http.route('/api/v1/products/<int:product_id>', type='http',
auth='none', methods=['GET'], csrf=False)
def get_product(self, product_id, **kwargs):
user, error = self._authenticate()
if error:
return error
product = request.env['product.product'].with_user(user).browse(product_id)
if not product.exists():
return self._error_response('Product not found', 404)
return self._json_response({
'data': product.read(['name', 'list_price', 'default_code', 'description'])[0]
})
@http.route('/api/v1/products', type='http', auth='none',
methods=['POST'], csrf=False)
def create_product(self, **kwargs):
user, error = self._authenticate()
if error:
return error
data = json.loads(request.httprequest.get_data())
required = ['name', 'list_price']
missing = [f for f in required if f not in data]
if missing:
return self._error_response(f'Missing fields: {", ".join(missing)}', 400)
product = request.env['product.product'].with_user(user).create({
'name': data['name'],
'list_price': data['list_price'],
'default_code': data.get('sku', ''),
})
return self._json_response({'data': {'id': product.id}}, status=201)Step 4: Add CORS Support
For browser-based API consumers (SPAs, mobile web apps), add CORS headers:
def _add_cors_headers(self, response):
response.headers['Access-Control-Allow-Origin'] = 'https://your-frontend.com'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-API-Key, Authorization'
return response
@http.route('/api/v1/products', type='http', auth='none',
methods=['OPTIONS'], csrf=False)
def preflight(self, **kwargs):
response = Response('', status=204)
return self._add_cors_headers(response)Step 5: Error Handling
def _json_response(self, data, status=200):
response = Response(
json.dumps(data, default=str),
content_type='application/json',
status=status
)
return self._add_cors_headers(response)
def _error_response(self, message, status):
return self._json_response({'error': message}, status=status)Best Practices
- Version your API: Use
/api/v1/prefix so you can introduce breaking changes in/api/v2/ - Use
with_user(): Never usesudo()for data access — let Odoo's record rules enforce permissions - Validate input: Check required fields, data types, and value ranges before creating records
- Paginate: Always paginate list endpoints — never return unbounded result sets
- Rate limit: Implement rate limiting for public APIs to prevent abuse
- Log API access: Record who accessed what and when for audit and debugging
Troubleshooting
404 on API Route
Ensure your module is installed and the controller file is imported in __init__.py. Odoo only registers routes from installed modules. Restart Odoo after adding new routes.
CSRF Validation Failed
Set csrf=False on your route decorator. CSRF protection is for web forms, not API endpoints. Authenticate API requests via API key or JWT instead.
JSON Serialization Error
Odoo records contain date, datetime, and binary fields that are not JSON-serializable by default. Use default=str in json.dumps() or convert these fields explicitly before serialization.
DeployMonkey API Development
DeployMonkey's AI agent can help you design, build, and debug custom REST APIs for your Odoo instance. Describe what data you need to expose or accept, and the agent generates the controller code, authentication logic, and documentation.