The Webhook Delivery Problem
Webhooks in Odoo notify external systems when events occur — a sale order is confirmed, an invoice is paid, or a customer is created. When webhook delivery fails, integrations break silently. The external system never receives the notification, and data goes out of sync.
Common Webhook Errors
# In Odoo logs:
WARNING odoo.addons.base_automation: Webhook delivery failed
HTTPError: 401 Unauthorized
# Or:
ConnectionError: HTTPSConnectionPool(host='api.example.com', port=443):
Max retries exceeded with url: /webhook/odoo
# Or:
TimeoutError: Request to https://api.example.com/webhook timed out after 10s
# Or:
HTTPError: 500 Internal Server ErrorHow Odoo Webhooks Work
Odoo can send webhooks through:
- Automated Actions (base_automation) — trigger on record create/write/delete/time
- Custom code — using
requestslibrary in server actions or cron jobs - Third-party modules — webhook modules from OCA or Odoo Apps
Cause 1: Endpoint URL Wrong or Unreachable
# The webhook URL is incorrect, the server is down, or DNS fails
# Fix: Test the endpoint independently
curl -v -X POST https://api.example.com/webhook/odoo \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Check DNS resolution:
nslookup api.example.com
# Check port accessibility:
telnet api.example.com 443
# Common mistakes:
# - HTTP vs HTTPS mismatch
# - Missing /path in URL
# - Trailing slash mismatch (/webhook vs /webhook/)
# - Using localhost (Odoo server can't reach your local machine)Cause 2: Authentication Failure (401/403)
# The receiving endpoint requires authentication
# Fix: Add authentication headers to the webhook request
# In Automated Action → Execute Python Code:
import requests
import json
url = 'https://api.example.com/webhook/odoo'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
# Or: 'X-Webhook-Secret': 'shared_secret_here'
}
payload = {
'event': 'sale_order_confirmed',
'order_id': record.id,
'order_name': record.name,
'amount': record.amount_total,
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
response.raise_for_status()
log(f'Webhook delivered: {response.status_code}', level='info')
except Exception as e:
log(f'Webhook failed: {e}', level='error')Cause 3: Timeout
The receiving endpoint takes too long to respond, and Odoo times out.
# Default timeout for requests library is None (wait forever)
# But Odoo worker limits may kill the request
# Fix 1: Set an explicit timeout
response = requests.post(url, json=payload, timeout=30)
# Fix 2: The receiving endpoint should return 200 immediately
# and process the webhook payload asynchronously
# Most webhook receivers follow this pattern:
# 1. Receive POST
# 2. Validate payload
# 3. Return 200 OK
# 4. Process data in background
# Fix 3: Use Odoo cron for async delivery
# Instead of sending synchronously during record save,
# queue the webhook and send via a cron jobCause 4: SSL Certificate Issues
# Self-signed or expired certificates cause delivery failure
# Error: SSLError: [SSL: CERTIFICATE_VERIFY_FAILED]
# Fix 1: Fix the certificate on the receiving endpoint
# Use Let's Encrypt for free valid certificates
# Fix 2 (development only): Disable SSL verification
response = requests.post(url, json=payload, verify=False, timeout=10)
# WARNING: Never disable SSL verification in productionCause 5: Payload Too Large
# Sending too much data in the webhook payload
# Fix: Send only essential fields, not entire records
# BAD:
payload = {'order': record.read()[0]} # Sends everything
# GOOD:
payload = {
'event': 'order_confirmed',
'order_id': record.id,
'order_ref': record.name,
'customer': record.partner_id.name,
'amount': record.amount_total,
'currency': record.currency_id.name,
}
# If the receiver needs more data, they should fetch it via APICause 6: Webhook Runs on Every Write
An automated action set to trigger on "On Update" fires on every field change, flooding the endpoint.
# Fix: Add conditions to the automated action
# Automated Action settings:
# Trigger: On Update
# Before Update Filter: [("state", "=", "draft")]
# Apply on: [("state", "=", "sale")]
# This fires only when state changes from draft to sale
# In Python code, check specific fields:
if 'state' not in record._fields:
return
# Or use env.context to check which fields changedBuilding a Reliable Webhook System
# Recommended pattern: Queue + Retry
# 1. Create a webhook queue model:
class WebhookQueue(models.Model):
_name = 'webhook.queue'
url = fields.Char(required=True)
payload = fields.Text(required=True)
state = fields.Selection([
('pending', 'Pending'),
('sent', 'Sent'),
('failed', 'Failed'),
], default='pending')
attempts = fields.Integer(default=0)
last_error = fields.Text()
# 2. Automated action queues the webhook instead of sending:
env['webhook.queue'].sudo().create({
'url': 'https://api.example.com/webhook',
'payload': json.dumps(payload),
})
# 3. Cron job processes the queue with retries:
# Retry pattern: attempt 1 immediately, 2 after 5 min, 3 after 30 min
# Mark as failed after 3 attemptsTesting Webhooks
# 1. Use a webhook testing service:
# https://webhook.site — gives you a temporary URL to receive webhooks
# Set this URL in your Odoo automated action to verify delivery
# 2. Use ngrok for local testing:
# ngrok http 3000 — creates a public URL for your local server
# Use the ngrok URL as the webhook endpoint
# 3. Check delivery in Odoo logs:
grep -i 'webhook\|delivery.*failed' /var/log/odoo/odoo-server.log | tail -20