Maintainers
This module is part of the ecosoft-odoo/ecosoft-addons project on GitHub.
You are welcome to contribute.
This module provides a standard webhook framework for Odoo with full request/response logging and config-driven outbound push notifications.
Inbound (External → Odoo)
Outbound (Odoo → External)
Table of contents
Go to Settings > Technical > Parameters > System Parameters to adjust the following keys:
| Key | Default | Description |
|---|---|---|
| webhook.preview_limit | 2000 | Maximum characters stored in the preview fields. Payloads longer than this are also saved as a full JSON attachment. |
| webhook.create_data_log | True | Enable logging for /api/create_data |
| webhook.update_data_log | True | Enable logging for /api/update_data |
| webhook.create_update_data_log | True | Enable logging for /api/create_update_data |
| webhook.search_data_log | True | Enable logging for /api/search_data |
| webhook.call_function_log | True | Enable logging for /api/call_function |
| webhook.rollback_state_failed | 1 | Roll back the transaction when the API response is not successful |
| webhook.rollback_except | 1 | Roll back the transaction when an unhandled exception occurs |
| webhook.ignore_checkcompany_model | [] | JSON list of model names excluded from company-scoped record lookup |
Go to Settings > Technical > API Configuration > Outbound Webhook Rules to configure outbound push rules.
| Field | Description |
|---|---|
| Model | The Odoo model to watch (e.g. sale.order) |
| Trigger Domain | Odoo domain evaluated after write(). Webhook fires when a record transitions into matching the domain. Uses the domain widget - select a model first to get field suggestions. |
| Endpoint Source | Static URL - always POST to the configured URL. Record Callback URL - use the callback_url stored from the inbound request. |
| Endpoint URL | Required when Endpoint Source is Static URL. |
| Payload Fields | JSON list of field names to include. Supports field{sub1,sub2} for relational expansion. Leave empty to send {"id": <record_id>} only. |
| Authorization Header | Optional Authorization header value sent with every outbound request, e.g. Bearer <token>. |
Every API call is logged under Settings > Technical > API Configuration > API Logs. Each log record shows:
Authenticate via /web/session/authenticate before calling any route:
{ "jsonrpc": "2.0", "method": "call", "params": { "db": "<db_name>", "login": "<username>", "password": "<password>" } }
Alternative - API Key: send Authorization: Bearer <api_key> on every request. No session call needed.
| Field type | Format | Example |
|---|---|---|
| many2one | {"<lookup_field>": "<value>"} | {"name": "Customer A"} or {"id": 5} |
| many2many | {"mode": "add"|"replace", "records": [...]} (mode defaults to "replace") | {"records": [{"name": "Tag1"}]} |
| one2many | [{<field>: <value>, ...}, ...] | [{"product_id": {"name": "A"}, "qty": 1}] |
Multiple many2many items sharing the same lookup field are batched into a single DB query.
Pass optional callback_url to enable outbound push when the record’s state changes later.
{ "params": { "model": "<model name>", "vals": { "callback_url": "https://your-system/webhook", "payload": { "<field1>": "<value1>", "<many2one_field_id>": {"name": "<value>"}, "<many2many_field_ids>": {"mode": "replace", "records": [{"name": "<val1>"}]}, "<one2many_field_ids>": [ {"<field>": "<value>", "<nested_m2o_id>": {"name": "<value>"}} ] }, "auto_create": { "<many2one_field_id>": {"name": "<value>"} }, "result_field": ["<field1>"] } } }
{ "params": { "model": "<model name>", "vals": { "search_key": {"<key_field>": "<value>"}, "payload": { "<field1>": "<value1>", "<many2one_field_id>": {"name": "<value>"} }, "result_field": ["<field1>"] } } }
{ "params": { "model": "<model name>", "vals": { "search_key": {"<key_field>": "<value>"}, "payload": { "<field1>": "<value1>", "<many2one_field_id>": {"id": 5}, "<many2many_field_ids>": {"mode": "add", "records": [{"name": "<val1>"}]} }, "result_field": ["<field1>"] } } }
Use field{subfield1,subfield2} to expand relational fields inline.
{ "params": { "model": "<model name>", "vals": { "payload": { "search_field": [ "<field1>", "<m2o_field>{<subfield1>,<subfield2>}", "<o2m_field>{<subfield1>}" ], "search_domain": "[('<field>', '<operator>', '<value>')]", "limit": 10, "order": "<field1> asc, <field2> desc" } } } }
{ "params": { "model": "account.move", "vals": { "search_key": {"id": 26}, "payload": { "method": "action_post", "context": {"lang": "th_TH"} } } } }
Add attachment_ids at any payload level:
"attachment_ids": [{"name": "<filename>", "datas": "<base64>"}]
When Odoo performs an action (confirm, validate, etc.), the outbound webhook automatically POSTs updated record data back to the external system - no per-model code required.
In any private addon, add one _inherit line:
from odoo import models class SaleOrder(models.Model): _name = "sale.order" _inherit = ["sale.order", "webhook.outbound.mixin"]
All outbound behaviour is driven by rules configured in the UI.
Go to Settings > Technical > API Configuration > Outbound Webhook Rules. See CONFIGURE.md for the full field reference.
Trigger domain examples:
# Simple [("state", "=", "sale")] # Multiple conditions [("state", "=", "done"), ("amount_total", ">", 100)] # Multiple accepted values [("state", "in", ["done", "validated"])]
The webhook fires only when a field in the domain is being written and the record matches the full domain after the write. This prevents re-triggering when unrelated fields are edited on an already-matching record.
Use field{sub1,sub2} syntax (same as search_data) to expand relational fields:
["name", "state", "currency_id{id,name,code}", "order_line{product_id,qty_done,price_unit}"]
Result posted to the external system:
{ "name": "SO001", "state": "sale", "currency_id": [{"id": 3, "name": "Thai Baht", "code": "THB"}], "order_line": [ {"product_id": 5, "qty_done": 2.0, "price_unit": 500.0} ] }
many2one fields expand to a list with one item (consistent with search_data behaviour).
Pass callback_url in the inbound create_data request. Odoo stores it linked to the created record. When the outbound rule fires with Endpoint Source = Record Callback URL, the system looks up that URL and POSTs to it.
{ "params": { "model": "sale.order", "vals": { "callback_url": "https://ext-system/webhook/so-status", "payload": { "partner_id": {"name": "ABC Co."}, "order_line": [{"product_id": {"name": "Product A"}, "product_uom_qty": 1}] } } } }
When the SO is confirmed → Odoo automatically POSTs to https://ext-system/webhook/so-status.
All outbound calls appear in API Logs with Log Type = Send. Failed calls are marked state = failed with the error in the response preview.
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed feedback.
Do not contact contributors directly about support or help with technical issues.
This module is part of the ecosoft-odoo/ecosoft-addons project on GitHub.
You are welcome to contribute.