Custom API Integrations in Odoo
API integrations play a vital role in connecting software platforms and enabling them to share data seamlessly. From powering e-commerce sites to synchronising inventory and automating data exchanges, APIs act as the bridge that makes systems interoperable.
Odoo, a powerful open-source ERP, provides a flexible framework for building custom API endpoints that address specific business needs. In this article, we’ll walk through how to create a custom API in Odoo using controllers. You’ll see how external applications can interact with your Odoo instance using the API to retrieve data such as sales orders, bills of materials (BOM), products, work centers, and stock levels. We’ll also cover how to secure these APIs with API key authentication.
API Key Authentication in Odoo
The first step in building a secure custom API is to add an API key mechanism. This key acts as a security token that external systems must include when interacting with your Odoo APIs.
To achieve this, an api_key field can be added to the res.users model. Each user then has a unique key linked to their account.
Generating and Authenticating API Keys
To generate a secure API key for each user, Python’s secrets library is a reliable choice. This ensures each key is cryptographically secure. Here’s how it’s done:
from odoo import models, fields, api
from odoo.exceptions import AccessDenied
import secrets
import json
class ResUsers(models.Model):
_inherit = 'res.users'
api_key = fields.Char(string="API Key", readonly=True)
def generate_api_key(self):
# Generates 64-character key
for user in self:
user.api_key = secrets.token_hex(32)
@api.model
def authenticate_api_key(self, api_key):
if not api_key:
raise AccessDenied("Missing API Key")
user = self.search(domain=[('api_key', '=', api_key)], limit=1)
if not user:
raise AccessDenied("Invalid API Key")
return user
- The generate_api_key method creates a unique 64-character hexadecimal key for each user.
- The authenticate_api_key method checks if the API key is valid. If the key is invalid or missing, Odoo raises an AccessDenied exception, blocking unauthorised access.
Creating Custom API Endpoints
With authentication in place, we can move to the main functionality: building API endpoints using Odoo’s Controller class.
Each endpoint will be responsible for fetching specific data (like sales orders, products, BOMs, etc.) and returning it as JSON. All APIs require the API key to be passed in the request header for authentication.
This API fetches a list of sale orders, including order lines with product details and quantities. It’s ideal for systems that need to retrieve customer order data.
class APISaleOrders(models.Controller):
@route('/api/sale_orders', auth='public', type='http', csrf=False, methods=['GET'])
def get_sale_orders(self, **kwargs):
api_key = request.httprequest.headers.get('API-Key')
user = request.env['res.users'].sudo().authenticate_api_key(api_key)
if not user:
return request.make_response(
json.dumps({"error": "Invalid API Key"}),
headers=[('Content-Type', 'application/json')],
status=401
)
sale_orders = request.env['sale.order'].sudo().search([])
data = []
for order in sale_orders:
order_lines = []
for line in order.order_line:
order_lines.append({
'product_id': line.product_id.id if line.product_id else '',
'product_name': line.product_id.name if line.product_id else '',
'description': line.name,
'qty': line.product_uom_qty,
'deliver_qty': line.qty_delivered,
'invoice_qty': line.qty_invoiced,
'uom_id': line.product_uom.id if line.product_uom else '',
'uom_name': line.product_uom.name if line.product_uom else '',
'package': line.product_packaging_id.name if line.product_packaging_id else '',
'package_qty': line.product_packaging_qty,
'price': line.price_unit,
})
data.append({
'order_name': order.name,
'partner_id': order.partner_id.id if order.partner_id else '',
'partner_name': order.partner_id.name if order.partner_id else '',
'order_date': order.date_order.isoformat() if order.date_order else None,
'delivery_date': order.commitment_date.isoformat() if order.commitment_date else None,
'order_lines': order_lines
})
return request.make_response(
json.dumps(data, indent=4),
headers=[('Content-Type', 'application/json')]
)
- The get_sale_orders method retrieves all sale orders and their associated lines, including detailed information about products and quantities.
- The API key is validated in the request header. If the key is invalid or missing, a 401 Unauthorised response is returned.
2. Bills of Materials (BOM) API
This API returns details of the Bills of Materials, including the components used in manufacturing.
class BOMController(Controller):
@route('/api/boms', auth='public', type='http', csrf=False, methods=['GET'])
def get_boms(self, **kwargs):
api_key = request.httprequest.headers.get('API-Key')
user = request.env['res.users'].sudo().authenticate_api_key(api_key)
if not user:
return request.make_response(
json.dumps({"error": "Invalid API Key"}),
headers=[('Content-Type', 'application/json')],
status=401
)
boms = request.env['mrp.bom'].sudo().search([])
data = []
for bom in boms:
bom_components = []
for line in bom.bom_line_ids:
bom_components.append({
'component_id': line.product_id.id if line.product_id else '',
'component_name': line.product_id.name if line.product_id else '',
'qty': line.product_qty,
'uom_id': line.product_uom_id.id if line.product_uom_id else '',
'uom_name': line.product_uom_id.name if line.product_uom_id else '',
})
data.append({
'id': bom.id,
'product_id': bom.product_tmpl_id.id if bom.product_tmpl_id else '',
'product_name': bom.product_tmpl_id.name if bom.product_tmpl_id else '',
'product_variant_id': bom.product_id.id if bom.product_id else '',
'product_variant_name': bom.product_id.name if bom.product_id else '',
'product_qty': bom.product_qty,
'code': bom.code,
'components': bom_components
})
return request.make_response(
json.dumps(data),
headers=[('Content-Type', 'application/json')],
status=200
)
- The get_boms method returns a list of BOMs, including their components, quantities, and units of measure.
- Ideal for manufacturing integrations where production data needs to be synchronised.
This API is used to retrieve detailed information about products, such as their internal reference, cost, weight, and category.
class APIProduct(Controller):
@route('/api/products', auth='public', type='http', csrf=False, methods=['GET'])
def get_products(self, **kwargs):
api_key = request.httprequest.headers.get('API-Key')
user = request.env['res.users'].sudo().authenticate_api_key(api_key)
if not user:
return request.make_response(
json.dumps({"error": "Invalid API Key"}),
headers=[('Content-Type', 'application/json')],
status=401
)
products = request.env['product.product'].sudo().search([])
data = []
for product in products:
data.append({
'name': product.name,
'type': product.type,
'internal_ref': product.default_code,
'cost': product.standard_price,
'barcode': product.barcode,
'category': product.categ_id.name if product.categ_id else '',
'purchase_unit': product.uom_po_id.name if product.uom_po_id else '',
'weight': product.weight,
'volume': product.volume,
'hs_code': product.hs_code,
'origin_of_good': product.country_of_origin.name if product.country_of_origin else '',
'expiration_time': product.expiration_time,
})
return request.make_response(
json.dumps(data),
headers=[('Content-Type', 'application/json')],
status=200
)
- The get_products method retrieves product data like name, category, price, barcode, and more.
- This is useful for e-commerce sites, catalogue systems, and reporting integrations.
For manufacturing operations, work centers are a critical part of the production workflow. This API provides detailed information about them.
class APIWorkCenter(Controller):
@route('/api/work_centers', auth='public', type='http', csrf=False, methods=['GET'])
def get_work_centers(self, **kwargs):
api_key = request.httprequest.headers.get('API-Key')
user = request.env['res.users'].sudo().authenticate_api_key(api_key)
if not user:
return request.make_response(
json.dumps({"error": "Invalid API Key"}),
headers=[('Content-Type', 'application/json')],
status=401
)
work_centers = request.env['mrp.workcenter'].sudo().search([])
data = []
for center in work_centers:
data.append({
'name': center.name,
'alternate_work_centers': ', '.join(
center.alternative_workcenter_ids.mapped('name')) if center.alternative_workcenter_ids else '',
'time_efficiency': center.time_efficiency,
'capacity': center.default_capacity,
'oee_target': center.oee_target,
'setup_time': center.time_start,
'cleanup_time': center.time_stop,
'cost_per_hour': center.costs_hour,
})
return request.make_response(
json.dumps(data),
headers=[('Content-Type', 'application/json')],
status=200
)
- The get_work_centers method provides detailed information on each work center, including its capacity, efficiency, and alternate centers.
- It helps external systems optimise manufacturing planning and scheduling.
Odoo’s Controller class, combined with API key authentication, makes it possible to build secure, scalable APIs that expose critical business data to external systems. This foundation supports integrations with third-party platforms while enabling automation, business intelligence, and cross-system analytics.
You can expand this setup further by developing additional controllers, introducing more complex business logic, or customising endpoints to fit unique workflows. With Odoo’s flexibility, your API architecture can continue to evolve as your integration needs grow.
Found this article useful?
Explore more development guides and solutions from our team.
Check out more posts