Quick Answer
Default XML-RPC is a 20-year-old nightmare for mobile apps and integrations. Developers spend weeks fighting with obscure XML parsing. Build modern REST API endpoints using Odoo's http.Controller with standard JSON. Mobile devs connect in 30 minutes using fetch() or axios. Fast, clean, and you control exactly what data is exposed.
The API Integration Problem
Your D2C brand is launching a mobile app. The app needs to fetch order status, update customer profiles, and push notifications.
The "Default" Way (XML-RPC)
You tell your mobile app developer to use Odoo's default XML-RPC API.
Result: They spend weeks fighting with a 20-year-old protocol, installing obscure XML parsing libraries, and complaining that they can't just "fetch JSON."
The "Pro" Way (Custom REST API)
You build a custom controller in Odoo that accepts standard JSON and returns standard JSON.
Result: The mobile dev connects in 30 minutes using fetch() or axios. The app is fast, the code is clean, and you control exactly what data is exposed.
We've implemented 150+ Odoo systems. The ones that integrate seamlessly with Shopify, mobile apps, and 3PLs? They use custom REST endpoints. The ones relying solely on XML-RPC for frontend integration? They are slow, fragile, and a nightmare to maintain.
The Architecture: Odoo's http.Controller
Odoo has a built-in web server (based on Werkzeug). You can expose endpoints just like you would in Flask or Django using the http.Controller class.
File Structure
custom_api/
├── __init__.py
├── controllers/
│ ├── __init__.py
│ └── main.py <-- Your API logic lives here
├── models/
└── security/
✓ Important: Make sure to import the controllers folder in your root __init__.py.
Step 1: The Basic Endpoint Structure
Here is the skeleton of a robust API controller.
from odoo import http
from odoo.http import request, Response
import json
import logging
_logger = logging.getLogger(__name__)
class CustomApi(http.Controller):
@http.route('/api/v1/orders', type='http', auth='public', methods=['GET'], csrf=False)
def get_orders(self, **kw):
"""
Endpoint to fetch orders.
URL: GET /api/v1/orders?customer_id=123
"""
try:
# 1. Authentication (Simulated Token Check)
api_key = request.httprequest.headers.get('Authorization')
if not self._check_auth(api_key):
return self._response({'error': 'Unauthorized'}, 401)
# 2. Logic
customer_id = kw.get('customer_id')
if not customer_id:
return self._response({'error': 'Missing customer_id'}, 400)
# Use sudo() carefully if auth='public' to access records
orders = request.env['sale.order'].sudo().search([
('partner_id', '=', int(customer_id))
])
# 3. Serialization (Odoo records -> JSON)
data = []
for order in orders:
data.append({
'id': order.id,
'name': order.name,
'amount': order.amount_total,
'state': order.state,
'date': str(order.date_order),
})
return self._response({'status': 'success', 'data': data}, 200)
except Exception as e:
_logger.error("API Error: %s", str(e))
return self._response({'error': 'Internal Server Error'}, 500)
def _check_auth(self, token):
# Implement your token validation logic here
# Example: Compare against a token stored in res.company
valid_token = request.env.company.sudo().x_api_token
return token == f"Bearer {valid_token}"
def _response(self, data, status=200):
return Response(
json.dumps(data),
status=status,
headers={'Content-Type': 'application/json'}
)
Key Concepts Explained
1. @http.route Configuration
This decorator defines how the URL is accessed.
| Parameter | Description |
|---|---|
| route | The URL path (e.g., /api/v1/orders) |
| type='http' | Full control over Request/Response (essential for REST) |
| auth='user' | Requires logged-in Odoo session (for internal portals) |
| auth='public' | Accessible by anyone (implement your own token auth) |
| csrf=False | Disable CSRF check (crucial for external API access) |
2. Authentication
Do not rely on Odoo's session cookies for server-to-server communication. Use a Token or API Key in the Header.
✓ Pattern: Store a "Secret Token" on your res.company model. In your controller, check request.httprequest.headers.get('Authorization') against this token.
3. Serialization (The Hard Part)
You cannot simply return json.dumps(orders). Odoo recordsets are Python objects, not dictionaries. You must iterate through them and build a list of dictionaries.
⚠️ Tip: For datetime fields, cast them to str() or .isoformat(), otherwise json.dumps will crash.
Handling Different HTTP Methods
POST (Creating Data)
Used for creating leads, orders, or customers.
@http.route('/api/v1/leads', type='http', auth='public', methods=['POST'], csrf=False)
def create_lead(self, **kw):
try:
# Parse JSON body
data = json.loads(request.httprequest.data)
name = data.get('name')
email = data.get('email')
if not name or not email:
return self._response({'error': 'Missing name or email'}, 400)
# Create Record
lead = request.env['crm.lead'].sudo().create({
'name': name,
'email_from': email,
'type': 'lead'
})
return self._response({
'status': 'created',
'id': lead.id
}, 201)
except Exception as e:
return self._response({'error': str(e)}, 500)
PUT (Updating Data)
Used for updating inventory or status.
@http.route('/api/v1/orders/<int:order_id>', type='http', auth='public', methods=['PUT'], csrf=False)
def update_order(self, order_id, **kw):
# Odoo automatically parses <int:order_id> from the URL
data = json.loads(request.httprequest.data)
order = request.env['sale.order'].sudo().browse(order_id)
if not order.exists():
return self._response({'error': 'Order not found'}, 404)
# Update logic
if 'note' in data:
order.write({'note': data['note']})
return self._response({'status': 'updated'}, 200)
Best Practices for Production APIs
✓ Strict Error Handling: Never return a generic Odoo HTML error page (the "Traceback" screen) to a JSON client. Wrap everything in try...except and return JSON with appropriate HTTP status codes (400, 401, 404, 500).
✓ Versioning: Always include /v1/ in your route. When you inevitably change the data structure, you can launch /v2/ without breaking existing mobile apps.
✓ Rate Limiting: Odoo doesn't rate limit by default. If you expect high traffic, handle rate limiting at the Nginx/Reverse Proxy level, not in Python code.
✓ Sudo Usage: When using auth='public', request.env belongs to the "Public User" who usually has zero access. You must use .sudo() to read/write data. Be extremely careful. Validate all inputs before passing them to search() or write().
✓ Performance: Do not return binary fields (like product images) in list endpoints. It will kill your mobile app's performance. Create a separate endpoint just for fetching images.
HTTP Methods & Status Codes
| HTTP Method | Purpose | Success Code | Example |
|---|---|---|---|
| GET | Read/fetch data | 200 OK | GET /api/v1/orders |
| POST | Create new record | 201 Created | POST /api/v1/leads |
| PUT | Update existing record | 200 OK | PUT /api/v1/orders/123 |
| DELETE | Delete record | 204 No Content | DELETE /api/v1/orders/123 |
Action Items: Build Your REST API
Plan Your API
❏ Define the exact endpoints you need (e.g., GET /orders, POST /customer)
❏ Determine the authentication method (API Key vs. Odoo Login)
❏ List the fields the external app actually needs (don't dump the whole database)
Implement
❏ Create the controllers/main.py file
❏ Implement the _response helper to ensure consistent JSON formatting
❏ Build the endpoints using type='http' and csrf=False
❏ Add robust logging using _logger
Test
❏ Use Postman or curl to test endpoints
❏ Test failure scenarios (bad JSON, invalid IDs, missing tokens)
❏ Verify that your headers are correct (Content-Type: application/json)
Frequently Asked Questions
Should I use XML-RPC or build a custom REST API in Odoo?
Custom REST API. XML-RPC is a 20-year-old protocol that's painful for modern integrations. Build custom endpoints with http.Controller using standard JSON. Mobile devs connect in 30 minutes vs. weeks with XML-RPC.
How do I authenticate external API calls to Odoo?
Store a secret token on res.company. In your controller, check request.httprequest.headers.get('Authorization') against this token. Don't rely on session cookies for server-to-server communication.
Why do I need csrf=False in Odoo API routes?
Odoo blocks POST requests without a CSRF token by default (browser security). External systems (mobile apps, 3PLs) can't provide CSRF tokens. Use csrf=False for public APIs, but validate authentication carefully with API keys.
How do I convert Odoo recordsets to JSON?
You cannot use json.dumps(orders) directly. Recordsets are Python objects, not dicts. Iterate through records and build a list of dictionaries. For datetime fields, use str(field) or field.isoformat().
Free API Architecture Review
Don't let a sloppy API ruin your mobile app launch. We'll analyze your integration requirements, review your controller security logic, advise on the most efficient data structures for mobile consumption, and help you debug connection issues with 3rd party apps. Connecting Odoo to the outside world shouldn't be painful.
