Skip to content

15. Integration Methods: Connecting Odoo with External Systems

Four Ways to Exchange Data with Odoo

MethodDirectionBest For
Webhooks (Incoming)External → OdooReceiving data from external systems
Webhooks (Outgoing)Odoo → ExternalNotifying external systems
JSON/2 APIBidirectionalModern app integration (Odoo 19+)
XML-RPC / JSON-RPCBidirectionalLegacy (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

  1. Create Automated Action on Sale Order model
  2. Set Trigger: On webhook
  3. Copy the Secret URL generated
  4. In Target Record, write code to parse incoming data
  5. 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 payload variable
  • 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

  1. Create Automated Action on Sale Order
  2. Set Trigger: State is set to "sale"
  3. Add Server Action: Send Webhook Notification
  4. Set Webhook URL: https://warehouse.yourcompany.com/api/orders
  5. 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/json

JSON/2 API Characteristics

FeatureDetails
Endpoint/json/2/{model}/{method}
HTTP MethodPOST only (GET returns 405)
AuthAuthorization: Bearer {api_key} header
Content-Typeapplication/json (required)
Request BodyJSON object with method parameters
ResponseClean JSON (no RPC envelope)
RecordsetsReturned as list[int] (IDs only, not full records)

Common Operations

Parameter Structure

The JSON/2 API uses two categories of parameters:

  • ids and context: 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 TypeExampleCallable via API?
Public methodaction_confirm()Yes
Private method (underscore prefix)_compute_total()No - raises AccessError
Private decorator@api.private methodsNo - 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:

  1. Use the API Documentation Browser at /doc (requires admin login - "Technical Documentation" group)
  2. 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

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:

  1. Go to Settings > Users & Companies > Users
  2. Open your user profile
  3. Click Account Security tab (or "API Keys" section)
  4. Click New API Key
  5. Enter a description (e.g., "Mobile App", "BI Dashboard")
  6. Select duration (1 Day to 1 Year, or Persistent for admins)
  7. Click Generate Key
  8. 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 TypeMax DurationPersistent Keys
Regular UserDefined by group's api_key_durationNo
System AdminUnlimitedYes

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:

ScopeAccess
None (default)Full access to all RPC methods
Specific scopeLimited 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

StatusMeaning
200Success
400Bad Request - invalid parameters
401Unauthorized - invalid or missing API key
403Forbidden - insufficient permissions
404Not Found - model or method doesn't exist
422Unprocessable Entity - invalid method call
500Internal 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

ErrorCauseSolution
the model 'X' does not existInvalid model nameCheck spelling, use technical name
cannot call X.Y with idsCalled @api.model method with idsRemove ids from request
Private methods cannot be called remotelyCalled method starting with _Use public methods only (no underscore prefix)
AccessErrorUser lacks permissionCheck access rights for API user
ValidationErrorConstraint violatedCheck 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:

  1. Install the api_doc module
  2. Navigate to https://your-odoo.com/doc
  3. Browse models, fields, and methods
  4. 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-RPCJSON/2 Equivalent
execute_kw(db, uid, pwd, model, method, args)POST /json/2/{model}/{method}
Session-based authBearer token auth
xmlrpc/2/common authenticateGenerate API key in UI
RPC envelope responseDirect 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?

FeatureIncoming WebhookOutgoing WebhookJSON/2 API
DirectionExternal → OdooOdoo → ExternalBidirectional
TriggerExternal systemOdoo eventExternal app
AuthenticationSecret URLNone (target system)Bearer token
ComplexityLowLowMedium
Configure in UIYesYesNo (code required)
StatusSupportedSupportedRecommended

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 NeedRecommended MethodWhy
E-commerce → Create Odoo ordersIncoming WebhookE-commerce pushes when placed
Payment gateway → Update invoiceIncoming WebhookGateway notifies on payment
Odoo order → Notify warehouseOutgoing WebhookOdoo event triggers notification
Mobile app accessing Odoo dataJSON/2 APIApp needs full CRUD
Custom BI dashboardJSON/2 APIDashboard pulls data on demand
Middleware syncing two systemsJSON/2 APIFull 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:

CommandFormatAction
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:

  1. Create the wizard with context specifying target records
  2. Call the action method on the created wizard ID
  3. 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:

Download Postman Collection

Setup:

  1. Import the JSON file into Postman or Insomnia
  2. Set baseUrl variable to your Odoo instance URL
  3. Set apiKey variable to your Bearer token
  4. 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:

🚀 Launch API 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

  1. Go to Security > WAF > Rate limiting rules
  2. Create rule:
    • If: URI Path contains /json/2/
    • Then: Block for 60 seconds
    • When rate exceeds: 100 requests per minute

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 exhausted

Rate Limiting Recommendations

ScenarioRecommended LimitWhy
Mobile app5-10 req/s per userProtect against runaway clients
Batch sync20-50 req/sHigher throughput for server-to-server
Webhooks100 req/minPrevent webhook storms
Public API1000 req/hourPrevent 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

  1. Batch operations - Create/update multiple records in one call
  2. Select only needed fields - Don't fetch entire records
  3. Use domains efficiently - Filter server-side, not client-side
  4. Cache static data - Products, categories don't change often
  5. Handle errors gracefully - Implement retry logic for transient failures
  6. Use connection pooling - Reuse HTTP connections for multiple requests
  7. Implement circuit breakers - Stop calling failing endpoints temporarily