この記事は現在英語版のみです。翻訳は近日公開予定です。
By the end of this recipe, your Odoo 19 instance will expose custom REST API endpoints — secured with API key authentication, rate-limited, JSON-validated, and documented via OpenAPI/Swagger — ready for third-party integrations to consume. Skill required: Python developer with REST API basics. Time required: 90 minutes per endpoint family. ECOSIRE has built REST APIs for Odoo modules consumed by mobile apps, partner integrations, and B2B customer portals, and the recipe below is the playbook.
The trap most teams fall into: exposing endpoints via auth='public' without rate limiting or input validation. The endpoint becomes a target for credential-stuffing or DoS within hours of going live. The recipe below adds defense-in-depth from the start.
What you will need
- Odoo version: 17, 18, or 19. The
http.ControllerAPI is identical. - Custom module.
- HTTPS in production (redis or memory backend for rate limiting).
- Time: 90 minutes per endpoint family.
Step-by-step
1. Define the API key model
from odoo import models, fields, api
import secrets
import hashlib
class ApiKey(models.Model):
_name = 'api.key'
_description = 'API Key'
name = fields.Char(required=True)
user_id = fields.Many2one('res.users', required=True)
key_hash = fields.Char()
last_used = fields.Datetime()
rate_limit_per_min = fields.Integer(default=60)
is_active = fields.Boolean(default=True)
@api.model
def generate(self, user_id, name):
plain_key = secrets.token_urlsafe(32)
record = self.create({
'user_id': user_id,
'name': name,
'key_hash': hashlib.sha256(plain_key.encode()).hexdigest(),
})
return plain_key # Show ONCE; store the hash only
@api.model
def authenticate(self, plain_key):
if not plain_key:
return None
h = hashlib.sha256(plain_key.encode()).hexdigest()
record = self.search([('key_hash', '=', h), ('is_active', '=', True)], limit=1)
if record:
record.last_used = fields.Datetime.now()
return record
Verification: self.env['api.key'].generate(2, 'Test') returns a 43-char string and stores its SHA256.
2. Build the controller with authentication
from odoo import http
from odoo.http import request
from werkzeug.exceptions import Unauthorized, BadRequest
class CustomerApiController(http.Controller):
def _authenticate(self):
api_key = request.httprequest.headers.get('X-API-Key')
if not api_key:
raise Unauthorized('Missing X-API-Key header')
record = request.env['api.key'].sudo().authenticate(api_key)
if not record:
raise Unauthorized('Invalid API key')
# Switch the env to the API user
request.update_env(user=record.user_id.id)
return record
@http.route('/api/v1/customers', auth='public', csrf=False, methods=['GET'], type='http')
def list_customers(self, **kwargs):
api_key = self._authenticate()
# Rate limit
if not self._check_rate_limit(api_key):
return request.make_response(
json.dumps({'error': 'rate_limit_exceeded'}),
headers=[('Content-Type', 'application/json')],
status=429,
)
# Pagination params
limit = min(int(kwargs.get('limit', 50)), 200)
offset = int(kwargs.get('offset', 0))
partners = request.env['res.partner'].search(
[('is_company', '=', True)],
limit=limit, offset=offset,
)
result = [{
'id': p.id,
'name': p.name,
'email': p.email,
'phone': p.phone,
'country': p.country_id.code,
} for p in partners]
return request.make_response(
json.dumps({'results': result, 'count': len(result)}),
headers=[('Content-Type', 'application/json')],
)
Verification: curl -H "X-API-Key: YOUR_KEY" https://your-odoo.com/api/v1/customers?limit=5 returns 5 customers.
3. Add rate limiting
from datetime import datetime, timedelta
def _check_rate_limit(self, api_key):
"""Token-bucket rate limit per API key. Stores counters in ir.config_parameter
for simplicity; for high-traffic, use Redis."""
ICP = request.env['ir.config_parameter'].sudo()
now = datetime.utcnow()
bucket_key = f'rate.{api_key.id}.{now.strftime("%Y%m%d%H%M")}'
current = int(ICP.get_param(bucket_key, '0'))
if current >= api_key.rate_limit_per_min:
return False
ICP.set_param(bucket_key, str(current + 1))
return True
For production-scale, swap ir_config_parameter for Redis with TTL. Verification: hit the endpoint 61 times in a minute (with default rate=60); the 61st returns 429.
4. Validate input strictly
from cerberus import Validator
@http.route('/api/v1/customers', auth='public', csrf=False, methods=['POST'], type='json')
def create_customer(self, **kwargs):
api_key = self._authenticate()
schema = {
'name': {'type': 'string', 'required': True, 'minlength': 1, 'maxlength': 200},
'email': {'type': 'string', 'regex': r'^[^@]+@[^@]+\.[^@]+$'},
'phone': {'type': 'string', 'maxlength': 50},
'country_code': {'type': 'string', 'minlength': 2, 'maxlength': 2},
}
v = Validator(schema)
if not v.validate(kwargs):
return {'error': 'validation', 'details': v.errors}
country = request.env['res.country'].search([('code', '=', kwargs.get('country_code'))], limit=1)
partner = request.env['res.partner'].sudo().create({
'name': kwargs['name'],
'email': kwargs.get('email'),
'phone': kwargs.get('phone'),
'country_id': country.id if country else False,
})
return {'id': partner.id, 'name': partner.name}
Verification: a POST without name returns {"error":"validation","details":{"name":["required field"]}}.
5. Handle errors uniformly
Wrap every endpoint in error handling that returns clean JSON:
def _api_error_handler(self, exc, status=500):
return request.make_response(
json.dumps({'error': str(exc), 'type': type(exc).__name__}),
headers=[('Content-Type', 'application/json')],
status=status,
)
@http.route('/api/v1/customers/<int:partner_id>', auth='public', csrf=False, methods=['GET'], type='http')
def get_customer(self, partner_id, **kwargs):
try:
self._authenticate()
partner = request.env['res.partner'].sudo().browse(partner_id)
if not partner.exists():
return self._api_error_handler('Not found', 404)
return request.make_response(
json.dumps({'id': partner.id, 'name': partner.name}),
headers=[('Content-Type', 'application/json')],
)
except Unauthorized as e:
return self._api_error_handler(e, 401)
except Exception as e:
return self._api_error_handler(e, 500)
Verification: hitting a non-existent ID returns 404 with clean JSON.
6. Add OpenAPI documentation
Generate a swagger.yaml file from your endpoint definitions:
openapi: 3.0.0
info:
title: Odoo Custom API
version: 1.0.0
servers:
- url: https://your-odoo.com/api/v1
paths:
/customers:
get:
summary: List customers
parameters:
- name: limit
in: query
schema: {type: integer, default: 50, maximum: 200}
- name: offset
in: query
schema: {type: integer, default: 0}
security:
- ApiKeyAuth: []
responses:
'200':
description: Array of customers
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
Host it on /api/docs via a controller that returns the YAML:
@http.route('/api/docs', auth='public', methods=['GET'])
def api_docs(self):
return request.make_response(
open('/path/to/swagger.yaml').read(),
headers=[('Content-Type', 'application/yaml')],
)
Verification: paste the YAML into Swagger Editor; it renders correctly.
7. Versioning strategy
Always include the version in the URL: /api/v1/.... When breaking changes are needed, ship /api/v2/... alongside. Don't break v1 for at least 12 months.
Verification: clients on v1 keep working; new clients on v2 get the new schema.
8. Test with real clients
Build a Postman collection with example requests for every endpoint. Share it with integration partners. Test from at least one real client (mobile app, Zapier integration, partner platform) before declaring v1 stable.
Common mistakes
auth='public'without API key check. Anyone can hit the endpoint anonymously.- Returning Odoo recordset objects directly. They're not JSON-serializable. Always extract dict.
- Forgetting CSRF=False. Odoo's default CSRF check breaks POST/PUT for non-browser clients.
- No rate limiting. Endpoint becomes a DoS target.
- Leaking internal errors. Production should return generic messages with correlation IDs; details go to server log.
Going further
JWT instead of API keys: for client-side identity propagation, use JWT signed by Odoo. Useful for mobile apps with per-user tokens. Each user logs in, gets a JWT, and the JWT carries their permissions. Easier to revoke than a long-lived API key.
OAuth 2.0: full OAuth flow with authorization_code grant for third-party apps. Allows customers to grant access to their Odoo to your app without sharing credentials. Standard pattern for SaaS integrations.
Webhook outbound: pair the inbound API with outbound webhooks so customers are notified when records change. They subscribe to events; your Odoo POSTs to their URL. Critical for keeping integrated systems in sync.
API analytics: log every request to a dedicated table; build a Power BI dashboard showing call volume, latency, error rates per endpoint. Monthly review meeting where the team identifies slow endpoints to optimize.
SDK generation: from the OpenAPI spec, auto-generate client SDKs in Python, JavaScript, Go, Java. Tools like openapi-generator produce idiomatic clients. ECOSIRE ships SDKs for our REST APIs; customers integrate in minutes instead of days.
API key rotation: implement key rotation with overlapping validity periods. Old key remains valid for 30 days after a new one is issued, giving customers time to switch.
Per-endpoint scopes: an API key isn't all-or-nothing. Add scopes (customers:read, customers:write, invoices:read) and check each endpoint requires the right scope.
Request/response logging: full payload logging for sensitive endpoints (financial, PII). Helps with debugging and audit but careful about PII retention policies.
API versioning roadmap: publish a deprecation calendar. Customers on v1 get 12 months notice when v2 is the only supported version.
Idempotency keys: for write endpoints, accept an Idempotency-Key header and de-dupe within 24 hours. Stripe-style. Critical for financial endpoints.
GraphQL alternative: for clients that need flexible querying, expose a GraphQL endpoint alongside REST. The OCA graphql_base module is a starting point.
Health check endpoint: /api/v1/health returns DB connectivity, queue depth, dependent service status. Used by load balancers and monitoring.
Sandbox environment: many B2B integrations need a sandbox to test against. Maintain a sandbox.api.yourdomain.com with the same endpoints but a separate database.
Developer portal: customers register, generate keys, browse docs, see their usage stats. ECOSIRE built this for our Odoo APIs; it scales support better than email-based key issuance.
For full REST API platform including OpenAPI generation, SDK building, and developer-portal, ECOSIRE custom Odoo development builds the complete stack. Pair this with how to build a Zapier integration.
Frequently Asked Questions
Should I use type='json' or type='http'?
type='json' auto-parses JSON bodies and serializes responses; useful for pure-JSON APIs. type='http' gives full control over headers and content types; useful for webhooks and unusual content types.
How do I add CORS for a frontend SPA?
Add Access-Control-Allow-Origin headers to the response and handle OPTIONS preflight:
@http.route('/api/v1/customers', auth='public', csrf=False, methods=['OPTIONS'], type='http')
def cors_preflight(self):
return request.make_response('', headers=[
('Access-Control-Allow-Origin', 'https://your-frontend.com'),
('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'),
('Access-Control-Allow-Headers', 'X-API-Key,Content-Type'),
])
How do I handle file uploads?
request.httprequest.files['file'] gives you the uploaded file. Convert to Odoo ir.attachment if needed.
What about GraphQL?
Odoo doesn't have native GraphQL. The OCA graphql_base module is a starting point but for production, REST is more battle-tested.
For complex API platforms including SDK generation, developer onboarding, and partner ecosystems, ECOSIRE custom Odoo development ships fixed-price engagements. Or read how to build a Zapier integration for an example consumer of these endpoints.
執筆者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
関連記事
Odoo フォーム ビューにカスタム ボタンを追加する方法 (2026)
Odoo 19 フォーム ビューにカスタム アクション ボタンを追加します: Python アクション メソッド、ビューの継承、条件付き可視性、確認ダイアログ。製造テスト済み。
Studio を使用せずに Odoo にカスタムフィールドを追加する方法 (2026)
Odoo 19 のカスタム モジュールを介してカスタム フィールドを追加します: モデル継承、ビュー拡張、計算フィールド、ストア/非ストアの決定。コードファースト、バージョン管理。
外部レイアウトを使用して Odoo にカスタム レポートを追加する方法
web.external_layout: QWeb テンプレート、paperformat、アクション バインディングを使用して、Odoo 19 でブランド化された PDF レポートを構築します。印刷ロゴ + フッターのオーバーライド付き。