Appearance
15. Integration Methods: Connecting Odoo with External Systems
Four Ways to Exchange Data with Odoo
| Method | Direction | Best For |
|---|---|---|
| Webhooks (Incoming) | External → Odoo | Receiving data from external systems |
| Webhooks (Outgoing) | Odoo → External | Notifying external systems |
| JSON/2 API | Bidirectional | Modern app integration (Odoo 19+) |
| XML-RPC / JSON-RPC | Bidirectional | Legacy (deprecated in Odoo 19) |
Incoming Webhooks: External Systems → Odoo
Incoming webhooks allow external systems to send data INTO Odoo by making a simple HTTP request to a special URL.
Location: Settings > Technical > Automation > Automated Actions (Trigger: On webhook)
Example: Receive Orders from E-commerce
- Create Automated Action on Sale Order model
- Set Trigger: On webhook
- Copy the Secret URL generated
- In Target Record, write code to parse incoming data
- Share the URL with your e-commerce developer
How Incoming Webhooks Work
- URL Format: /web/hook/
- Method: Accepts GET and POST requests
- Authentication: Public endpoint - security through URL secrecy
- Data Format: Receives JSON or form data via
payloadvariable - Response: Returns
{"status": "ok"}or{"status": "error"}
Best Practices
- Security: Keep the webhook URL secret
- Regenerate URL: Use "Rotate Secret" if compromised
- Validation: Always validate incoming data
- Idempotency: Check if record exists to avoid duplicates
Outgoing Webhooks: Odoo → External Systems
Outgoing webhooks allow Odoo to send data to external systems via HTTP POST when events happen.
Location: Settings > Technical > Actions > Server Actions (Action Type: Send Webhook)
Example: Notify Warehouse of New Orders
- Create Automated Action on Sale Order
- Set Trigger: State is set to "sale"
- Add Server Action: Send Webhook Notification
- Set Webhook URL: https://warehouse.yourcompany.com/api/orders
- Select Fields to Send: Order Number, Customer, Lines, Address
How Outgoing Webhooks Work
- Method: HTTP POST with JSON payload
- Payload structure:json
{ "_id": 42, "_model": "sale.order", "_name": "S00042", "field1": "value1", "field2": "value2" } - Synchronous: Blocks the Odoo transaction until complete (timeout ~10s)
- No Retry: If external system is down, webhook fails silently
- No Queueing: Consider using a message queue for reliability
JSON/2 API: Modern RESTful Integration (Odoo 19+)
JSON/2 is Odoo's modern, RESTful-style API for external applications.
Endpoint Format
POST https://your-odoo.com/json/2/{model}/{method}Authentication: Bearer Token
All JSON/2 requests require an API key passed as a Bearer token:
http
POST /json/2/res.partner/search_read
Authorization: Bearer odoo_api_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/jsonJSON/2 API Characteristics
| Feature | Details |
|---|---|
| Endpoint | /json/2/{model}/{method} |
| HTTP Method | POST only (GET returns 405) |
| Auth | Authorization: Bearer {api_key} header |
| Content-Type | application/json (required) |
| Request Body | JSON object with method parameters |
| Response | Clean JSON (no RPC envelope) |
| Recordsets | Returned as list[int] (IDs only, not full records) |
Common Operations
Parameter Structure
The JSON/2 API uses two categories of parameters:
idsandcontext: Top-level parameters handled by the controller- Method parameters: Passed directly to the Odoo method (e.g.,
domain,fields,vals)
Search and Read Records
http
POST /json/2/res.partner/search_read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domain": [["is_company", "=", true]],
"fields": ["name", "email", "phone"],
"limit": 10,
"offset": 0
}Read Specific Records
http
POST /json/2/res.partner/read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [1, 2, 3],
"fields": ["name", "email"]
}Create Records
http
POST /json/2/res.partner/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"name": "New Customer",
"email": "customer@example.com",
"is_company": true
}
}Update Records
http
POST /json/2/res.partner/write
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [42],
"vals": {
"phone": "+1 555-1234"
}
}Delete Records
http
POST /json/2/res.partner/unlink
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [42]
}Call Any Public Method
http
POST /json/2/sale.order/action_confirm
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [15]
}Method Accessibility Rules
Only public methods can be called via API. Odoo enforces these rules:
| Method Type | Example | Callable via API? |
|---|---|---|
| Public method | action_confirm() | Yes |
| Private method (underscore prefix) | _compute_total() | No - raises AccessError |
| Private decorator | @api.private methods | No - raises AccessError |
| Helper/internal methods | _get_default_values() | No - raises AccessError |
Private Methods Are Blocked
Any method starting with _ (underscore) is considered private and cannot be called remotely. This includes:
- Computed field methods:
_compute_*,_inverse_*,_search_* - Constraint methods:
_check_* - Onchange methods:
_onchange_* - Internal helpers:
_get_*,_prepare_*,_create_*
Attempting to call a private method returns:
json
{
"message": "Private methods (such as 'sale.order._compute_amount') cannot be called remotely."
}Finding Callable Methods
To discover which methods you can call on a model:
- Use the API Documentation Browser at
/doc(requires admin login - "Technical Documentation" group) - Look for methods in the source code that:
- Don't start with
_ - Are not decorated with
@api.private - Common patterns:
action_*,button_*,create,write,read,search,unlink
- Don't start with
Using Context
Pass context to modify behavior (language, company, etc.):
http
POST /json/2/product.product/search_read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domain": [["sale_ok", "=", true]],
"fields": ["name", "list_price"],
"context": {
"lang": "fr_FR",
"allowed_company_ids": [1, 2]
}
}API Key Management
Generating API Keys
API keys are generated per-user from their profile:
- Go to Settings > Users & Companies > Users
- Open your user profile
- Click Account Security tab (or "API Keys" section)
- Click New API Key
- Enter a description (e.g., "Mobile App", "BI Dashboard")
- Select duration (1 Day to 1 Year, or Persistent for admins)
- Click Generate Key
- Copy the key immediately - it's only shown once!
Security Note
- API keys are hashed (bcrypt) in the database - cannot be recovered
- Keys use the format:
odoo_api_+ 40 random characters - The API user's permissions apply to all API calls
- Revoke compromised keys immediately from the user profile
API Key Duration Limits
The maximum key duration depends on user group settings:
| User Type | Max Duration | Persistent Keys |
|---|---|---|
| Regular User | Defined by group's api_key_duration | No |
| System Admin | Unlimited | Yes |
Groups can set api_key_duration (in days) to limit how long users can create API keys for.
API Key Scopes
API keys can optionally have a scope that limits what they can access:
| Scope | Access |
|---|---|
None (default) | Full access to all RPC methods |
| Specific scope | Limited to methods matching that scope |
Automatic Key Cleanup
Expired API keys are automatically deleted by a scheduled action (_gc_user_apikeys).
Error Handling
HTTP Status Codes
| Status | Meaning |
|---|---|
200 | Success |
400 | Bad Request - invalid parameters |
401 | Unauthorized - invalid or missing API key |
403 | Forbidden - insufficient permissions |
404 | Not Found - model or method doesn't exist |
422 | Unprocessable Entity - invalid method call |
500 | Internal Server Error |
Error Response Format
Errors return JSON with details about the failure:
json
{
"message": "the model 'fake.model' does not exist"
}For validation errors, additional context may be included:
json
{
"message": "Missing required field: 'partner_id'",
"code": 400
}Common Errors
| Error | Cause | Solution |
|---|---|---|
the model 'X' does not exist | Invalid model name | Check spelling, use technical name |
cannot call X.Y with ids | Called @api.model method with ids | Remove ids from request |
Private methods cannot be called remotely | Called method starting with _ | Use public methods only (no underscore prefix) |
AccessError | User lacks permission | Check access rights for API user |
ValidationError | Constraint violated | Check required fields, constraints |
Code Examples
Python Client
python
import requests
ODOO_URL = "https://your-odoo.com"
API_KEY = "odoo_api_xxxxxxxxxxxxxxxxxxxxxxxx"
def odoo_api(model, method, **kwargs):
"""Call Odoo JSON/2 API"""
response = requests.post(
f"{ODOO_URL}/json/2/{model}/{method}",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json=kwargs,
)
response.raise_for_status()
return response.json()
# Search partners
partners = odoo_api(
"res.partner", "search_read",
domain=[["is_company", "=", True]],
fields=["name", "email"],
limit=10,
)
print(partners)
# Create a partner
new_id = odoo_api(
"res.partner", "create",
vals_list={"name": "New Customer", "email": "new@example.com"},
)
print(f"Created partner ID: {new_id}")
# Update a partner
odoo_api(
"res.partner", "write",
ids=[new_id],
vals={"phone": "+1 555-0000"},
)
# Delete a partner
odoo_api("res.partner", "unlink", ids=[new_id])JavaScript/Node.js Client
javascript
const ODOO_URL = "https://your-odoo.com";
const API_KEY = "odoo_api_xxxxxxxxxxxxxxxxxxxxxxxx";
async function odooApi(model, method, params = {}) {
const response = await fetch(
`${ODOO_URL}/json/2/${model}/${method}`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(params),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || response.statusText);
}
return response.json();
}
// Search partners
const partners = await odooApi("res.partner", "search_read", {
domain: [["is_company", "=", true]],
fields: ["name", "email"],
limit: 10,
});
console.log(partners);
// Create a partner
const newId = await odooApi("res.partner", "create", {
vals_list: { name: "New Customer", email: "new@example.com" },
});
console.log(`Created partner ID: ${newId}`);
// Update a partner
await odooApi("res.partner", "write", {
ids: [newId],
vals: { phone: "+1 555-0000" },
});
// Delete a partner
await odooApi("res.partner", "unlink", { ids: [newId] });cURL Examples
bash
# Search partners
curl -X POST "https://your-odoo.com/json/2/res.partner/search_read" \
-H "Authorization: Bearer odoo_api_xxx" \
-H "Content-Type: application/json" \
-d '{"domain": [["is_company", "=", true]], "fields": ["name"], "limit": 5}'
# Create a partner
curl -X POST "https://your-odoo.com/json/2/res.partner/create" \
-H "Authorization: Bearer odoo_api_xxx" \
-H "Content-Type: application/json" \
-d '{"vals_list": {"name": "Test Customer"}}'
# Update a partner
curl -X POST "https://your-odoo.com/json/2/res.partner/write" \
-H "Authorization: Bearer odoo_api_xxx" \
-H "Content-Type: application/json" \
-d '{"ids": [42], "vals": {"phone": "+1 555-1234"}}'
# Delete a partner
curl -X POST "https://your-odoo.com/json/2/res.partner/unlink" \
-H "Authorization: Bearer odoo_api_xxx" \
-H "Content-Type: application/json" \
-d '{"ids": [42]}'API Documentation Browser
Odoo 19 includes a built-in API documentation browser at /doc:
- Install the
api_docmodule - Navigate to
https://your-odoo.com/doc - Browse models, fields, and methods
- View method signatures and documentation
Bearer Access
The API doc is also available via Bearer auth at /doc-bearer/index.json for programmatic access.
Batch Operations
Bulk Create
http
POST /json/2/res.partner/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": [
{"name": "Customer 1", "email": "c1@example.com"},
{"name": "Customer 2", "email": "c2@example.com"},
{"name": "Customer 3", "email": "c3@example.com"}
]
}Returns: [id1, id2, id3]
Bulk Update
http
POST /json/2/res.partner/write
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [1, 2, 3, 4, 5],
"vals": {
"active": false
}
}Bulk Delete
http
POST /json/2/res.partner/unlink
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [10, 11, 12, 13]
}XML-RPC & JSON-RPC: Legacy APIs
Deprecation Notice
XML-RPC (/xmlrpc/2/object, /xmlrpc/2/common) and JSON-RPC (/jsonrpc) are deprecated starting Odoo 17 in favor of JSON/2.
- Odoo 17-19: Both APIs work but JSON/2 is recommended
- Future versions: Legacy APIs may be removed
Migrate existing integrations to JSON/2 API for long-term compatibility.
Migration Guide: XML-RPC to JSON/2
| XML-RPC | JSON/2 Equivalent |
|---|---|
execute_kw(db, uid, pwd, model, method, args) | POST /json/2/{model}/{method} |
| Session-based auth | Bearer token auth |
xmlrpc/2/common authenticate | Generate API key in UI |
| RPC envelope response | Direct JSON response |
Before (XML-RPC):
python
import xmlrpc.client
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
partners = models.execute_kw(db, uid, password, 'res.partner', 'search_read',
[[['is_company', '=', True]]], {'fields': ['name']})After (JSON/2):
python
import requests
response = requests.post(f'{url}/json/2/res.partner/search_read',
headers={'Authorization': f'Bearer {api_key}'},
json={'domain': [['is_company', '=', True]], 'fields': ['name']})
partners = response.json()Comparison: Which Method to Use?
| Feature | Incoming Webhook | Outgoing Webhook | JSON/2 API |
|---|---|---|---|
| Direction | External → Odoo | Odoo → External | Bidirectional |
| Trigger | External system | Odoo event | External app |
| Authentication | Secret URL | None (target system) | Bearer token |
| Complexity | Low | Low | Medium |
| Configure in UI | Yes | Yes | No (code required) |
| Status | Supported | Supported | Recommended |
Decision Guide
Use Incoming Webhooks when:
- External system pushes data to Odoo
- Configure in Odoo UI without coding
- Single records sent occasionally (e-commerce orders)
Use Outgoing Webhooks when:
- Odoo needs to notify external system
- Configure in Odoo UI without coding
- External system has HTTP endpoint ready
Use JSON/2 API when:
- Building custom app needing full Odoo access
- Need to search, read, create, update, delete
- Want modern, RESTful-style integration
Common Integration Scenarios
| Business Need | Recommended Method | Why |
|---|---|---|
| E-commerce → Create Odoo orders | Incoming Webhook | E-commerce pushes when placed |
| Payment gateway → Update invoice | Incoming Webhook | Gateway notifies on payment |
| Odoo order → Notify warehouse | Outgoing Webhook | Odoo event triggers notification |
| Mobile app accessing Odoo data | JSON/2 API | App needs full CRUD |
| Custom BI dashboard | JSON/2 API | Dashboard pulls data on demand |
| Middleware syncing two systems | JSON/2 API | Full control over sync logic |
Real-World Use Cases
These examples show complete business operations you can perform via API without any custom Odoo development.
Use Case 1: Create a Sale Order with Lines
Create a complete quotation with multiple product lines from an external system (e-commerce, mobile app, etc.):
http
POST /json/2/sale.order/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"partner_id": 15,
"partner_invoice_id": 15,
"partner_shipping_id": 16,
"date_order": "2024-01-15 10:30:00",
"client_order_ref": "ECOM-2024-0042",
"note": "Please deliver before noon",
"order_line": [
[0, 0, {
"product_id": 25,
"product_uom_qty": 2,
"price_unit": 99.99,
"discount": 10.0
}],
[0, 0, {
"product_id": 31,
"product_uom_qty": 1,
"price_unit": 249.00
}],
[0, 0, {
"product_id": 42,
"name": "Custom engraving service",
"product_uom_qty": 1,
"price_unit": 25.00
}]
]
}
}Response: 42 (the new sale order ID)
Relationship Commands (One2many/Many2many)
The [command, id, values] syntax manipulates related records:
| Command | Format | Action |
|---|---|---|
0 | [0, 0, {values}] | Create new linked record |
1 | [1, id, {values}] | Update existing record |
2 | [2, id, 0] | Delete record from database |
3 | [3, id, 0] | Unlink (remove relation, keep record) |
4 | [4, id, 0] | Link existing record |
5 | [5, 0, 0] | Unlink all (clear relation) |
6 | [6, 0, [ids]] | Replace all links with these IDs |
The third element is required but ignored for commands 2-5 (use 0).
Use Case 2: Complete Sales Workflow
Process a quote through the entire sales cycle:
Step 1: Confirm the Quotation → Sales Order
http
POST /json/2/sale.order/action_confirm
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [42]
}Step 2: Check Delivery Status
http
POST /json/2/stock.picking/search_read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domain": [["origin", "ilike", "SO042"]],
"fields": ["name", "state", "scheduled_date", "origin"]
}Finding Deliveries
Use the origin field (contains SO number) to find deliveries linked to a sale order. The field sale_id exists but requires the sale_stock module.
Step 3: Create Invoice from Sales Order
Use the invoicing wizard to create an invoice. This is a two-step process:
http
POST /json/2/sale.advance.payment.inv/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"advance_payment_method": "delivered"
},
"context": {
"active_ids": [42],
"active_model": "sale.order"
}
}Response: 7 (the wizard ID)
advance_payment_method options
delivered- Invoice for delivered quantities (regular invoice)percentage- Down payment (percentage)fixed- Down payment (fixed amount)
Then call the wizard's action using the returned wizard ID:
http
POST /json/2/sale.advance.payment.inv/create_invoices
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [7],
"context": {
"active_ids": [42],
"active_model": "sale.order"
}
}Wizard Pattern
Many Odoo operations use transient models (wizards). The pattern is:
- Create the wizard with context specifying target records
- Call the action method on the created wizard ID
- The wizard is automatically cleaned up after some time
Step 4: Post the Invoice
http
POST /json/2/account.move/action_post
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [15]
}Use Case 3: Create a Customer Invoice Directly
Create an invoice without a sales order (service billing, manual invoice):
http
POST /json/2/account.move/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"move_type": "out_invoice",
"partner_id": 15,
"invoice_date": "2024-01-15",
"invoice_date_due": "2024-02-15",
"ref": "Consulting Services - January 2024",
"invoice_line_ids": [
[0, 0, {
"name": "Strategy Consulting - 40 hours",
"quantity": 40,
"price_unit": 150.00
}],
[0, 0, {
"name": "Technical Implementation",
"quantity": 1,
"price_unit": 2500.00
}],
[0, 0, {
"name": "Travel Expenses",
"quantity": 1,
"price_unit": 450.00
}]
]
}
}Use Case 4: Register a Payment
Record a customer payment against an invoice using the payment wizard:
http
POST /json/2/account.payment.register/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"payment_date": "2024-01-20",
"amount": 8450.00,
"journal_id": 7
},
"context": {
"active_model": "account.move",
"active_ids": [15]
}
}Response: 3 (the wizard ID)
Then execute the payment using the wizard ID:
http
POST /json/2/account.payment.register/action_create_payments
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [3],
"context": {
"active_model": "account.move",
"active_ids": [15]
}
}This creates an account.payment record and automatically reconciles it with the invoice.
Use Case 5: Update Inventory Quantities
Adjust stock levels (e.g., after physical count):
Step 1: Find the Quant
http
POST /json/2/stock.quant/search_read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domain": [
["product_id", "=", 25],
["location_id", "=", 8]
],
"fields": ["id", "quantity", "inventory_quantity"]
}Step 2: Set the Inventory Quantity
http
POST /json/2/stock.quant/write
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [123],
"vals": {
"inventory_quantity": 50
}
}Step 3: Apply the Adjustment
http
POST /json/2/stock.quant/action_apply_inventory
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [123]
}Use Case 6: Create a Purchase Order
Order products from a supplier:
http
POST /json/2/purchase.order/create
Authorization: Bearer your-api-key
Content-Type: application/json
{
"vals_list": {
"partner_id": 8,
"order_line": [
[0, 0, {
"product_id": 25,
"name": "Raw Material A",
"product_qty": 100,
"price_unit": 12.50
}],
[0, 0, {
"product_id": 26,
"name": "Raw Material B",
"product_qty": 50,
"price_unit": 8.00
}]
]
}
}Confirm the purchase:
http
POST /json/2/purchase.order/button_confirm
Authorization: Bearer your-api-key
Content-Type: application/json
{
"ids": [10]
}Use Case 7: Search with Complex Filters
Find all draft sales orders over $1,000 from this month:
http
POST /json/2/sale.order/search_read
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domain": [
["state", "=", "draft"],
["amount_total", ">=", 1000],
["date_order", ">=", "2024-01-01"],
["date_order", "<", "2024-02-01"]
],
"fields": ["name", "partner_id", "amount_total", "date_order", "user_id"],
"order": "amount_total desc",
"limit": 50
}Domain Operators
Comparison: =, !=, >, >=, <, <=
List: in, not in
Pattern: like (case-sensitive), ilike (case-insensitive), =like, =ilike
Relational (Odoo 17+): any, not any (for x2many fields)
Logical: & (AND, default), | (OR), ! (NOT) - prefix notation:
json
["|", ["state", "=", "draft"], ["state", "=", "sent"]]Note: & is implicit between conditions. Use | or ! explicitly.
Use Case 8: Sync Customer Data
Keep your CRM synchronized with Odoo:
Upsert Pattern (Create or Update):
python
import requests
def sync_customer(external_id, customer_data):
"""Sync a customer from external system to Odoo"""
# Search for existing customer by external reference
existing = odoo_api("res.partner", "search_read",
domain=[["ref", "=", external_id]],
fields=["id"],
limit=1
)
vals = {
"name": customer_data["name"],
"email": customer_data["email"],
"phone": customer_data["phone"],
"street": customer_data["address"],
"city": customer_data["city"],
"ref": external_id, # Store external ID for future syncs
"customer_rank": 1,
}
if existing:
# Update existing
odoo_api("res.partner", "write",
ids=[existing[0]["id"]],
vals=vals
)
return existing[0]["id"]
else:
# Create new
return odoo_api("res.partner", "create", vals_list=vals)Postman / Insomnia Collection
Download a ready-to-use API collection for testing:
Setup:
- Import the JSON file into Postman or Insomnia
- Set
baseUrlvariable to your Odoo instance URL - Set
apiKeyvariable to your Bearer token - Start testing!
The collection includes requests for Partners, Products, Sale Orders, Invoices, Purchase Orders, Inventory, and Bulk Operations.
Interactive API Playground
Want to test API calls directly in your browser? Try our interactive playground:
The playground lets you:
- Connect to your Odoo instance with your API key
- Use pre-built templates for common operations
- Edit and execute custom JSON requests
- View responses with syntax highlighting
- Track your request history and stats
Security
Your API key is stored only in your browser's session storage and is sent directly to your Odoo instance. We never see or store your credentials.
Performance Considerations
Rate Limiting Strategies
Odoo does not have built-in API rate limiting. Implement protection at the infrastructure level:
Option 1: Nginx Rate Limiting
nginx
# /etc/nginx/conf.d/odoo-api.conf
# Define rate limit zone (10 requests/second per IP)
limit_req_zone $binary_remote_addr zone=odoo_api:10m rate=10r/s;
# Apply to API endpoints
location /json/2/ {
limit_req zone=odoo_api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://odoo_backend;
}Option 2: Cloudflare Rate Limiting
- Go to Security > WAF > Rate limiting rules
- Create rule:
- If: URI Path contains
/json/2/ - Then: Block for 60 seconds
- When rate exceeds: 100 requests per minute
- If: URI Path contains
Option 3: Application-Level (Python Client)
python
import time
class RateLimiter:
def __init__(self, calls_per_second=5):
self.calls_per_second = calls_per_second
self.last_call = 0
def wait(self):
elapsed = time.time() - self.last_call
wait_time = (1 / self.calls_per_second) - elapsed
if wait_time > 0:
time.sleep(wait_time)
self.last_call = time.time()
limiter = RateLimiter(calls_per_second=5)
def odoo_api(model, method, **kwargs):
limiter.wait() # Enforce rate limit
response = requests.post(
f"{ODOO_URL}/json/2/{model}/{method}",
headers={"Authorization": f"Bearer {API_KEY}"},
json=kwargs,
)
return response.json()Option 4: Token Bucket with Retry
python
import time
class TokenBucket:
def __init__(self, tokens_per_second=10, max_tokens=50):
self.tokens_per_second = tokens_per_second
self.max_tokens = max_tokens
self.tokens = max_tokens
self.last_update = time.time()
def acquire(self):
now = time.time()
# Add tokens based on time elapsed
self.tokens = min(
self.max_tokens,
self.tokens + (now - self.last_update) * self.tokens_per_second
)
self.last_update = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
def wait_for_token(self):
while not self.acquire():
time.sleep(0.1)
bucket = TokenBucket(tokens_per_second=10, max_tokens=50)
def odoo_api_with_retry(model, method, max_retries=3, **kwargs):
"""Call Odoo API with automatic retry and rate limiting."""
import requests # Ensure requests is imported
for attempt in range(max_retries):
bucket.wait_for_token()
try:
response = requests.post(
f"{ODOO_URL}/json/2/{model}/{method}",
headers={"Authorization": f"Bearer {API_KEY}"},
json=kwargs,
timeout=30
)
if response.status_code == 429:
wait = 2 ** attempt # Exponential backoff: 1s, 2s, 4s
time.sleep(wait)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
return None # All retries exhaustedRate Limiting Recommendations
| Scenario | Recommended Limit | Why |
|---|---|---|
| Mobile app | 5-10 req/s per user | Protect against runaway clients |
| Batch sync | 20-50 req/s | Higher throughput for server-to-server |
| Webhooks | 100 req/min | Prevent webhook storms |
| Public API | 1000 req/hour | Prevent abuse |
Connection Timeouts
Default HTTP timeout is typically 60 seconds. For long operations:
- Use background jobs (Automated Actions)
- Process in smaller batches
- Consider async patterns with webhooks
Best Practices
- Batch operations - Create/update multiple records in one call
- Select only needed fields - Don't fetch entire records
- Use domains efficiently - Filter server-side, not client-side
- Cache static data - Products, categories don't change often
- Handle errors gracefully - Implement retry logic for transient failures
- Use connection pooling - Reuse HTTP connections for multiple requests
- Implement circuit breakers - Stop calling failing endpoints temporarily