Ad – 728×90
💻 Website Development

Odoo Website Forms – Contact and Custom Forms

Website forms let visitors submit data from public pages — contact requests, lead generation, event registrations, or custom enquiries. Odoo provides a built-in website_form JavaScript widget for simple cases, and the full POST controller pattern for custom logic.

⏱️ 20 min 🎯 Intermediate 📅 Updated 2026
What you'll learn:
  • The built-in website_form widget and when to use it
  • Writing a custom POST controller for website form submissions
  • CSRF and SPAM protection on public forms
  • Handling file uploads and confirmation redirects

The website_form Widget

For standard forms that create records (leads, job applications, contact messages), the website_form JavaScript widget handles submission, validation, and SPAM protection automatically. Enable it by adding data-model-name to your form tag:

XML (QWeb)
<form id="contact_form"
      class="s_website_form"
      action="/website_form/"
      method="post"
      data-model-name="crm.lead"
      data-success-page="/contactus-thank-you">

  <input type="hidden" name="csrf_token"
         t-att-value="request.csrf_token()"/>

  <div class="form-group">
    <label for="contact_name">Name</label>
    <input type="text" class="form-control"
           name="contact_name" required="required"/>
  </div>

  <div class="form-group">
    <label for="email_from">Email</label>
    <input type="email" class="form-control"
           name="email_from" required="required"/>
  </div>

  <div class="form-group">
    <label for="description">Message</label>
    <textarea class="form-control" name="description" rows="5"/>
  </div>

  <button type="submit" class="btn btn-primary">Send</button>
</form>

The website_form widget posts to /website_form/{model_name} and creates a record using the form field names as model field names. The model must allow website form creation — configure this in Website → Configuration → Form Allowed Models.

Custom POST Controller

For custom logic (multi-step forms, complex validation, multiple model writes), write your own controller:

Python
from odoo import http
from odoo.http import request


class LibraryWebsite(http.Controller):

    @http.route('/books/request', type='http',
                auth='public', website=True, methods=['GET', 'POST'])
    def book_request_form(self, **kw):
        if request.httprequest.method == 'POST':
            return self._handle_book_request(kw)

        books = request.env['library.book'].sudo().search([
            ('website_published', '=', True)
        ])
        return request.render('my_module.book_request_form', {
            'books': books, 'errors': {}, 'values': {},
        })

    def _handle_book_request(self, values):
        errors = {}
        name = (values.get('visitor_name') or '').strip()
        email = (values.get('visitor_email') or '').strip()
        book_id = int(values.get('book_id') or 0)

        if not name:
            errors['visitor_name'] = 'Name is required.'
        if not email or '@' not in email:
            errors['visitor_email'] = 'Valid email required.'
        if not book_id:
            errors['book_id'] = 'Please select a book.'

        if errors:
            books = request.env['library.book'].sudo().search([
                ('website_published', '=', True)
            ])
            return request.render('my_module.book_request_form', {
                'books': books, 'errors': errors, 'values': values,
            })

        request.env['library.book.request'].sudo().create({
            'visitor_name': name,
            'visitor_email': email,
            'book_id': book_id,
        })
        return request.redirect('/books/request/thank-you')

CSRF Protection on Public Forms

All POST forms must include the CSRF token. For public (unauthenticated) pages, the token is still required:

XML (QWeb)
<form method="post" action="/books/request">
  <input type="hidden" name="csrf_token"
         t-att-value="request.csrf_token()"/>
  ...
</form>

Without the CSRF token, Odoo returns HTTP 403. The token is tied to the visitor's session, which Odoo creates automatically even for anonymous visitors.

Ad – 728×90

SPAM Protection

Public forms are targets for bot spam. Odoo's website_form widget adds a honeypot field automatically. For custom forms, implement your own protection:

  • Honeypot field — an invisible input; if filled in, it's a bot:
XML (QWeb)
<!-- Hidden from real users via CSS, filled by bots -->
<input type="text" name="website_url"
       style="display:none" tabindex="-1" autocomplete="off"/>
Python
# In controller: reject if honeypot filled
if values.get('website_url'):
    return request.redirect('/books/request')  # silently ignore bots

File Uploads

For file uploads, add enctype="multipart/form-data" to the form and access files in the controller:

Python
import base64

attachment = request.httprequest.files.get('document')
if attachment and attachment.filename:
    data = attachment.read()
    # Validate size (max 5 MB)
    if len(data) > 5 * 1024 * 1024:
        errors['document'] = 'File must be under 5 MB.'
    else:
        request.env['ir.attachment'].sudo().create({
            'name': attachment.filename,
            'datas': base64.b64encode(data),
            'res_model': 'library.book.request',
            'res_id': book_request.id,
        })
Key takeaways:
  • Use the website_form widget for simple record-creation forms — it handles SPAM and validation automatically
  • For custom logic, write a GET/POST controller with manual validation and a POST/Redirect/GET pattern
  • Always include csrf_token in every POST form
  • Validate file type and size before creating ir.attachment records

Frequently Asked Questions

When should I use website_form vs a custom POST controller?

Use website_form when you're creating a single record (lead, contact message, job application) and standard field mapping is sufficient. Use a custom controller when you need multi-model writes, complex validation, file uploads alongside record creation, or conditional logic based on submitted values.

How do I show a thank-you page after form submission?

Redirect to a dedicated thank-you URL after successful processing: return request.redirect('/my-form/thank-you'). Create a simple controller and template for that URL. This also prevents duplicate submissions on browser refresh (POST/Redirect/GET pattern).

Can I use reCAPTCHA with Odoo website forms?

Yes — the google_recaptcha module integrates reCAPTCHA v3 with the website_form widget. Configure the site key in Website → Configuration → Settings. For custom forms, call request.env['ir.recaptcha']._verify_recaptcha_token(token) in your controller.