All posts
Developer8 min read

Odoo HTTP controllers: building custom routes, JSON endpoints, and public pages

Odoo controllers let you add custom HTTP routes to the Odoo web server - public pages, JSON APIs, file downloads, and webhook receivers. This guide covers route definition, authentication modes, accessing the ORM from a controller, CSRF handling, and the gotchas that cause 403 errors or broken sessions.

What a Controller Is#

Odoo runs on top of werkzeug, a Python WSGI framework. A controller is a Python class that inherits from odoo.http.Controller and exposes methods decorated with @http.route. When a matching URL is requested, Odoo dispatches the call to the decorated method.

Controllers live in the controllers/ directory of a module. They are loaded automatically when the module is installed.


Basic Route Definition#

python
from odoo import http
from odoo.http import request

class MyController(http.Controller):

    @http.route('/my-module/hello', type='http', auth='public', website=True)
    def hello(self, **kwargs):
        return request.render('my_module.hello_template', {
            'message': 'Hello from Odoo',
        })

Key parameters of @http.route:

  • route: the URL path (string or list of strings); supports and path converters
  • type: 'http' for HTML/plain responses, 'json' for JSON-RPC (automatically parses request body and serializes response)
  • auth: authentication requirement (see below)
  • methods: list of allowed HTTP methods, e.g. ['GET', 'POST']
  • website: if True, injects website context (theme, multi-language support, etc.)
  • csrf: CSRF token checking; defaults to True for type='http' POST routes

Authentication Modes#

auth valueWho can access
'user'Logged-in internal users only (default for backend routes)
'public'Any visitor; session user is the public user if not logged in
'none'No session at all - use for webhooks and external API endpoints

With auth='public', request.env.user is the public user (no access rights). Use request.env(user=request.env.ref('base.user_admin').id) or sudo() to elevate privileges when needed, with care.

With auth='none', request.env is not available. You must build a database cursor manually - avoid this unless building low-level webhooks.


JSON Routes#

Use type='json' for API endpoints:

python
@http.route('/api/my-module/data', type='json', auth='user', methods=['POST'])
def get_data(self, record_id, **kwargs):
    record = request.env['my.model'].browse(record_id)
    return {
        'id': record.id,
        'name': record.name,
        'state': record.state,
    }

With type='json':

  • The request body is parsed as JSON-RPC 2.0 (Odoo's convention)
  • The return value is automatically serialized as JSON
  • Exceptions are caught and returned as JSON error responses
  • CSRF is not enforced (JSON routes are typically called programmatically)

Call it from JavaScript:

javascript
const result = await this.orm.call("my.model", "some_method", [recordId]);
// or via fetch for raw JSON-RPC:
fetch('/api/my-module/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { record_id: 42 } }),
});

Accessing the ORM#

Inside a controller method, use request.env to access the ORM:

python
@http.route('/api/orders/<int:order_id>', type='json', auth='user')
def get_order(self, order_id):
    order = request.env['sale.order'].browse(order_id)
    if not order.exists():
        return {'error': 'not found'}
    # request.env respects the current user's access rights
    return {'name': order.name, 'state': order.state}

request.env is scoped to the session user. Record rules and access rights apply. Use request.env['my.model'].sudo() to bypass them when the business logic requires it - document why.


Returning Responses#

Render a QWeb template#

python
return request.render('my_module.my_template', values_dict)

Return a file download#

python
pdf_bytes = generate_pdf()
return request.make_response(
    pdf_bytes,
    headers=[
        ('Content-Type', 'application/pdf'),
        ('Content-Disposition', 'attachment; filename="report.pdf"'),
    ]
)

Redirect#

python
return request.redirect('/destination-path')

CSRF Protection#

Odoo enforces CSRF tokens on HTTP POST routes by default. For routes called from external systems (webhooks, mobile apps, external APIs):

python
@http.route('/webhook/stripe', type='http', auth='none', methods=['POST'], csrf=False)
def stripe_webhook(self, **kwargs):
    payload = request.httprequest.data
    # validate Stripe signature before processing
    ...
    return 'OK'

Set csrf=False explicitly; never disable CSRF on routes that modify user data without an alternative authentication check.


Five Gotchas#

1. 403 on POST without CSRF token#

If a browser form POSTs to your route and the CSRF token is missing, Odoo returns 403. Include in your QWeb form, or set csrf=False only for non-browser callers.

2. auth='none' and missing database cursor#

With auth='none', there is no automatic database connection. If you need the ORM, open a cursor explicitly with odoo.registry(request.db).cursor() inside a with block.

3. Inherited routes require website=True on both#

If you override a website route in a child module, the child's @http.route decorator must also specify website=True. Omitting it removes the website context from the override.

4. Path converter type mismatch raises 404#

in the route pattern only matches numeric segments. A request with a non-numeric value silently returns 404 instead of a useful error.

5. Controllers are not reloaded without restarting Odoo#

In development, changes to controller files require a server restart (or --dev=reload). The --dev flag watches for file changes and reloads automatically; without it, edits to controllers are invisible until restart.


ERPeek can identify all custom controller routes defined in a codebase, the authentication mode of each, and which routes have CSRF disabled. Useful for security audits. See the contact page for a demo.

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

Get started free