Skip to content

Odoo Controllers & HTTP Routes: Complete Developer Guide

DeployMonkey Team · March 22, 2026 15 min read

What Are Controllers?

Controllers handle HTTP requests in Odoo. They define custom API endpoints, web pages, webhooks, and file download routes. Every URL your Odoo instance responds to (beyond the default web client) is handled by a controller.

Basic Controller

from odoo import http
from odoo.http import request


class MyController(http.Controller):

    @http.route('/api/hello', type='json', auth='public', methods=['POST'])
    def hello(self, **kwargs):
        name = kwargs.get('name', 'World')
        return {'message': f'Hello, {name}!'}

    @http.route('/my/page', type='http', auth='public', website=True)
    def my_page(self, **kwargs):
        return request.render('my_module.my_template', {
            'title': 'My Page',
        })

Route Parameters

ParameterValuesDefaultDescription
type'http', 'json''http'Response format
auth'user', 'public', 'none''user'Authentication required
methods['GET'], ['POST'], etc.['GET','POST']Allowed HTTP methods
websiteTrue/FalseFalseWebsite page (adds layout)
csrfTrue/FalseTrueCSRF protection
cors'*' or originNoneCORS header
sitemapTrue/False/funcTrueInclude in sitemap

Authentication Modes

# auth='user' — Requires logged-in Odoo user
@http.route('/api/orders', type='json', auth='user')
def get_orders(self):
    orders = request.env['sale.order'].search([])
    return [{'name': o.name, 'total': o.amount_total} for o in orders]

# auth='public' — Anyone can access (logged-in gets user env, anonymous gets public user)
@http.route('/api/products', type='json', auth='public')
def get_products(self):
    products = request.env['product.product'].sudo().search(
        [('sale_ok', '=', True)], limit=50
    )
    return [{'name': p.name, 'price': p.list_price} for p in products]

# auth='none' — No authentication at all (no request.env available)
@http.route('/api/health', type='http', auth='none')
def health(self):
    return 'OK'

type='json' vs type='http'

# type='json' — Request body is JSON, response is JSON
# Content-Type: application/json
# Request: {"jsonrpc": "2.0", "params": {"name": "Alice"}}
# Response: {"jsonrpc": "2.0", "result": {"message": "Hello!"}}

@http.route('/api/data', type='json', auth='public')
def get_data(self, **kwargs):
    return {'key': 'value'}  # Auto-wrapped in JSON-RPC response


# type='http' — Standard HTTP request/response
# Can return HTML, redirect, file download, or JSON manually

@http.route('/api/data', type='http', auth='public')
def get_data(self, **kwargs):
    data = json.dumps({'key': 'value'})
    return request.make_response(data, headers=[
        ('Content-Type', 'application/json'),
    ])

URL Parameters

# Path parameters
@http.route('/api/partners/<int:partner_id>', type='json', auth='user')
def get_partner(self, partner_id):
    partner = request.env['res.partner'].browse(partner_id)
    if not partner.exists():
        return {'error': 'Not found'}
    return {'name': partner.name, 'email': partner.email}

# String parameter
@http.route('/api/partners/<string:slug>', type='http', auth='public')
def get_partner_by_slug(self, slug):
    # ...

# Query parameters (from URL ?key=value)
@http.route('/api/search', type='http', auth='public')
def search(self, q='', limit=10, **kwargs):
    # q and limit come from ?q=test&limit=20
    results = request.env['product.product'].sudo().search(
        [('name', 'ilike', q)], limit=int(limit)
    )

File Upload

@http.route('/api/upload', type='http', auth='user', methods=['POST'], csrf=False)
def upload_file(self, file=None, **kwargs):
    if not file:
        return request.make_response(
            json.dumps({'error': 'No file'}), status=400
        )
    # Read file content
    content = file.read()
    filename = file.filename

    # Create attachment
    attachment = request.env['ir.attachment'].create({
        'name': filename,
        'datas': base64.b64encode(content),
        'type': 'binary',
    })
    return request.make_response(
        json.dumps({'id': attachment.id, 'name': filename}),
        headers=[('Content-Type', 'application/json')]
    )

File Download

@http.route('/api/download/<int:attachment_id>', type='http', auth='user')
def download_file(self, attachment_id):
    attachment = request.env['ir.attachment'].browse(attachment_id)
    if not attachment.exists():
        return request.not_found()

    content = base64.b64decode(attachment.datas)
    return request.make_response(content, headers=[
        ('Content-Type', attachment.mimetype or 'application/octet-stream'),
        ('Content-Disposition', f'attachment; filename="{attachment.name}"'),
    ])

Error Handling

from odoo.exceptions import AccessError, ValidationError
from werkzeug.exceptions import NotFound, Forbidden

@http.route('/api/orders/<int:order_id>', type='json', auth='user')
def get_order(self, order_id):
    try:
        order = request.env['sale.order'].browse(order_id)
        if not order.exists():
            return {'error': 'Order not found', 'code': 404}
        return {'name': order.name, 'total': order.amount_total}
    except AccessError:
        return {'error': 'Access denied', 'code': 403}
    except Exception as e:
        return {'error': str(e), 'code': 500}

CORS for External APIs

# Allow cross-origin requests (for SPA/mobile apps)
@http.route('/api/public/products', type='http', auth='none',
            cors='*', methods=['GET', 'OPTIONS'])
def public_products(self):
    # Handle preflight OPTIONS request
    products = request.env['product.product'].sudo().search(
        [('sale_ok', '=', True)], limit=20
    )
    data = json.dumps([{'name': p.name} for p in products])
    return request.make_response(data, headers=[
        ('Content-Type', 'application/json'),
    ])

Common Mistakes

  • Forgetting csrf=False for external APIs — POST requests from external clients will fail without it
  • Using sudo() carelessly — Bypasses all security; only use for public data endpoints
  • Not handling auth='public' correctly — Anonymous users get the public user's environment, which has very limited access
  • Returning HTML from type='json' — JSON routes must return JSON-serializable data
  • Not catching exceptions — Unhandled errors show Odoo tracebacks to API consumers