Skip to content

How to Build Custom REST API Endpoints in Odoo: Developer Guide

DeployMonkey Team · March 23, 2026 12 min read

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, None

Bearer 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 use sudo() 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.