Skip to content

Odoo API Best Practices: Versioning, Rate Limiting & Security

DeployMonkey Team · March 22, 2026 15 min read

Why API Best Practices Matter

Custom Odoo APIs power mobile apps, SPAs, integrations, and webhooks. A poorly designed API causes: security vulnerabilities, performance issues, breaking changes, and integration nightmares. Following best practices from the start saves months of rework.

1. URL Versioning

# Version your API URLs:
# /api/v1/partners     ← current version
# /api/v2/partners     ← next version (breaking changes)

# In Odoo controller:
class ApiV1Controller(http.Controller):
    @http.route('/api/v1/partners', type='json', auth='public')
    def get_partners_v1(self, **kwargs):
        # v1 implementation
        pass

class ApiV2Controller(http.Controller):
    @http.route('/api/v2/partners', type='json', auth='public')
    def get_partners_v2(self, **kwargs):
        # v2 implementation (different response format)
        pass

# Rule: never break v1 after clients use it
# Deprecate gradually: announce → warn → sunset

2. Consistent Response Format

# Always return the same structure:
{
    "success": true,
    "data": {"partners": [...]},
    "meta": {"total": 150, "limit": 20, "offset": 0}
}

# Error response:
{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Email is required",
        "details": {"field": "email"}
    }
}

# Never mix formats — clients should know exactly what to parse

3. Pagination

# Always paginate list endpoints:
@http.route('/api/v1/partners', type='json', auth='user')
def get_partners(self, limit=20, offset=0, **kwargs):
    limit = min(int(limit), 100)  # Cap at 100
    offset = max(int(offset), 0)
    
    domain = [('is_company', '=', True)]
    total = request.env['res.partner'].search_count(domain)
    partners = request.env['res.partner'].search(
        domain, limit=limit, offset=offset, order='name'
    )
    return {
        'success': True,
        'data': [p._to_api_dict() for p in partners],
        'meta': {'total': total, 'limit': limit, 'offset': offset}
    }

4. Input Validation

# Validate ALL input before processing:
def _validate_partner_data(self, data):
    errors = []
    if not data.get('name'):
        errors.append({'field': 'name', 'message': 'Name is required'})
    if data.get('email') and '@' not in data['email']:
        errors.append({'field': 'email', 'message': 'Invalid email format'})
    if data.get('phone') and len(data['phone']) < 7:
        errors.append({'field': 'phone', 'message': 'Phone too short'})
    return errors

@http.route('/api/v1/partners', type='json', auth='user', methods=['POST'])
def create_partner(self, **kwargs):
    errors = self._validate_partner_data(kwargs)
    if errors:
        return {'success': False, 'error': {'code': 'VALIDATION_ERROR', 'details': errors}}
    
    partner = request.env['res.partner'].create({
        'name': kwargs['name'],
        'email': kwargs.get('email'),
    })
    return {'success': True, 'data': partner._to_api_dict()}

5. Rate Limiting

# Prevent abuse and protect server resources:
import time
from collections import defaultdict

_rate_limits = defaultdict(list)  # {ip: [timestamps]}

def _check_rate_limit(self, ip, max_requests=60, window=60):
    """Allow max_requests per window seconds."""
    now = time.time()
    _rate_limits[ip] = [t for t in _rate_limits[ip] if now - t < window]
    if len(_rate_limits[ip]) >= max_requests:
        return False  # Rate limited
    _rate_limits[ip].append(now)
    return True

@http.route('/api/v1/data', type='json', auth='public')
def get_data(self, **kwargs):
    ip = request.httprequest.remote_addr
    if not self._check_rate_limit(ip):
        return {'success': False, 'error': {'code': 'RATE_LIMITED', 'message': 'Too many requests'}}

6. Error Handling

# Catch and format all errors:
@http.route('/api/v1/partners/', type='json', auth='user')
def get_partner(self, partner_id):
    try:
        partner = request.env['res.partner'].browse(partner_id)
        if not partner.exists():
            return {'success': False, 'error': {'code': 'NOT_FOUND', 'message': 'Partner not found'}}
        return {'success': True, 'data': partner._to_api_dict()}
    except AccessError:
        return {'success': False, 'error': {'code': 'FORBIDDEN', 'message': 'Access denied'}}
    except Exception as e:
        _logger.error("API error: %s", e, exc_info=True)
        return {'success': False, 'error': {'code': 'INTERNAL_ERROR', 'message': 'An error occurred'}}

7. Authentication

# Use API keys or JWT — not passwords:

# API Key in header:
# Authorization: Bearer api_key_here

def _authenticate_api_key(self):
    auth_header = request.httprequest.headers.get('Authorization', '')
    if not auth_header.startswith('Bearer '):
        return None
    api_key = auth_header[7:]
    user = request.env['res.users'].sudo().search([
        ('api_key_ids.key', '=', api_key)
    ], limit=1)
    return user or None

8. Serialization

# Create a _to_api_dict() method on your models:
def _to_api_dict(self):
    self.ensure_one()
    return {
        'id': self.id,
        'name': self.name,
        'email': self.email or None,
        'phone': self.phone or None,
        'created_at': self.create_date.isoformat() if self.create_date else None,
    }

# Benefits:
# - Consistent output format
# - Control exactly which fields are exposed
# - Handle None/False conversion
# - Date formatting in one place

Checklist

  1. Version your URLs (/api/v1/...)
  2. Consistent response format (success, data, error, meta)
  3. Paginate all list endpoints (limit, offset, total)
  4. Validate all input before processing
  5. Rate limit public endpoints
  6. Catch and format all errors (never expose tracebacks)
  7. Use API keys or JWT (not passwords)
  8. Create _to_api_dict() for serialization
  9. Log all API calls for debugging
  10. Document your API (OpenAPI/Swagger)

DeployMonkey

DeployMonkey's API follows all these best practices — versioned endpoints, JWT authentication, rate limiting, input validation, and consistent error handling. The AI agent helps you build custom APIs for your Odoo modules with the same standards.