Quick Answer
Stop wasting server resources with polling. Old school: External apps ask Odoo every 5 minutes "Do you have new orders?" Result: wasted API calls, rate limits, 4:59 minute delays. Modern: Webhooks = Odoo pushes data the millisecond an event happens (HTTP POST to external URL). Result: instant welcome emails, real-time Slack alerts, 200ms reaction time vs. 10 minutes. Use Automated Actions + Python requests.post() with timeout=5, error handling, shared secret authentication. Zero paid apps required.
The Synchronization Problem
Your D2C brand uses a modern tech stack: Shopify for frontend, Klaviyo for marketing, Slack for team comms, and Odoo as the ERP core.
The "Old School" Approach (Polling)
Your external apps ask Odoo every 5 minutes: "Do you have new orders? Do you have new orders? How about now?"
Result: Wasted server resources, API rate limit hits, and a 4:59 minute delay before your marketing team knows about a VIP purchase.
The "Modern" Approach (Webhooks)
Odoo acts like a sniper. The millisecond an order is confirmed, Odoo pushes the data to Klaviyo.
Result: Instant welcome emails. Real-time Slack alerts. Zero wasted API calls.
We've implemented 150+ Odoo systems. The ones that feel "magical"? They use webhooks. The ones that feel sluggish? They rely on polling. That's the difference between a system that reacts in 200ms and one that reacts in 10 minutes.
What is a Webhook?
A webhook is simply Odoo making a HTTP POST request to an external URL when a specific event happens (create, update, delete). It carries a "payload" (data) about the event.
Common D2C Use Cases
| Event | Webhook Action |
|---|---|
| New Customer | Push customer data to CRM/Marketing tool instantly |
| Order Shipped | Push tracking number to custom SMS gateway |
| Low Stock | Post message to #procurement Slack channel |
| Refund Processed | Trigger loyalty point deduction in loyalty app |
Step 1: The Setup (Automated Actions)
We don't need a complex module structure for this. We can use Odoo's built-in Automated Actions (requires the base_automation module installed).
Navigation
1. Enable Developer Mode
2. Go to Settings → Technical → Automation → Automated Actions
3. Click Create
Configuration Example
| Field | Value |
|---|---|
| Name | Webhook: New High Value Order |
| Model | Sale Order (sale.order) |
| Trigger | On Creation & Update |
| Trigger Fields | State (fire when order confirmed) |
| Before Update Domain | State != Sale |
| After Update Domain | State == Sale AND Amount Total > 500 |
Step 2: The Python Logic
In the "Action To Do" section, select Execute Python Code.
Here is the robust, production-ready code. This uses the requests library (standard in Odoo environments).
# PRODUCTION-READY WEBHOOK CODE
import requests
import json
import logging
# 1. Setup Logging
_logger = logging.getLogger(__name__)
# 2. Configuration
WEBHOOK_URL = "https://api.external-system.com/v1/odoo-events"
API_SECRET = "your_secure_secret_token" # Store in system parameters in prod!
# 3. Build the Payload
# Extract exactly what the external system needs. Don't dump the whole record.
payload = {
"event": "order.confirmed",
"timestamp": str(datetime.datetime.now()),
"data": {
"order_id": record.id,
"order_name": record.name,
"customer_email": record.partner_id.email,
"total_amount": record.amount_total,
"currency": record.currency_id.name,
"line_items": []
}
}
# Add line items loop
for line in record.order_line:
payload["data"]["line_items"].append({
"sku": line.product_id.default_code,
"quantity": line.product_uom_qty,
"price": line.price_unit
})
# 4. Send the Request
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_SECRET}",
"User-Agent": "Odoo-Webhook-Bot/1.0"
}
try:
# Set a timeout! Essential to prevent Odoo from hanging
response = requests.post(
WEBHOOK_URL,
data=json.dumps(payload, default=str),
headers=headers,
timeout=5
)
# 5. Handle Response
if response.status_code == 200:
_logger.info(f"Webhook Success for Order {record.name}")
else:
_logger.error(f"Webhook Failed for Order {record.name}. Status: {response.status_code}, Response: {response.text}")
# Optional: Create an activity for the admin to check this failure
record.activity_schedule(
'mail.mail_activity_data_warning',
summary='Webhook Failed',
note=f'Server returned {response.status_code}'
)
except requests.exceptions.Timeout:
_logger.error(f"Webhook Timeout for Order {record.name}")
except Exception as e:
_logger.error(f"Webhook Error for Order {record.name}: {str(e)}")
Critical Best Practices for Webhooks
1. Always Use Timeouts
The Nightmare: Your external API (e.g., SMS gateway) goes down. It doesn't reject the connection; it just hangs. Your Odoo user clicks "Confirm" on an order. The spinner spins... and spins... for 60 seconds until the browser times out. The user thinks Odoo is broken.
The Fix: requests.post(..., timeout=5). If the remote server doesn't answer in 5 seconds, cut the cord and log the error. Do not block the user.
2. Don't Dump the Whole Record
Odoo records are heavy. Sending record.read() can result in massive JSON payloads containing binary image data or irrelevant fields.
The Fix: Construct a specific dictionary (payload) containing only the fields the endpoint needs. It's faster and cleaner.
3. Handle Date Serialization
Python datetime objects are not JSON serializable by default.
The Fix: Use json.dumps(..., default=str). This automatically converts date objects to ISO strings.
4. Security (The Shared Secret)
How does the receiving server know this request came from your Odoo and not a hacker?
The Fix: Include a shared secret in the headers (e.g., Authorization: Bearer SECRET_TOKEN). The receiving server validates this token before processing the data.
Advanced Pattern: Asynchronous Webhooks (Queue Job)
If you are sending webhooks that might take a long time to process, or if you are sending hundreds at once, don't do it in the user's thread.
Use the Queue Job module (OCA).
Define the Job
@job
def send_webhook_job(self, record_id):
record = self.browse(record_id)
# ... Run the request code here ...
Trigger the Job
Inside your Automated Action, simply call:
record.with_delay().send_webhook_job(record.id)
Benefit: The user clicks "Confirm," the transaction saves instantly, and the webhook fires in the background worker process. If it fails, the Queue Job module handles retries automatically (e.g., retry 3 times every 10 minutes).
Real-World Scenario: Slack Notification on Big Sale
Goal: Post to #sales-wins when an order > $5,000 is confirmed.
payload = {
"text": f"🚨 *BIG SALE ALERT* 🚨\nOrder: record.name\nCustomer: record.partner_id.name\nAmount: record.amount_total"
}
WEBHOOK_URL = "https://hooks.slack.com/services/T000/B000/XXXX"
Result: Your sales team gets immediate visibility and motivation. It costs $0 and 10 lines of code.
Polling vs. Webhooks Comparison
| Aspect | Polling (Old School) | Webhooks (Modern) |
|---|---|---|
| Response Time | 4:59 minute delay (worst case) | 200ms (instant) |
| Server Load | Constant API calls every 5 min | Zero load until event happens |
| API Rate Limits | Hits limits quickly | No wasted calls |
| Implementation | External cron job required | Automated Action + Python |
Action Items: Implement Webhooks
Identify Triggers
❏ List the events that need to leave Odoo (Order Confirmed, Invoice Paid, Stock Move)
❏ Determine where this data needs to go
Build & Test
❏ Use a service like Webhook.site to generate a temporary URL
❏ Point your Odoo Automated Action to that URL
❏ Trigger the event in Odoo
❏ Inspect the JSON payload on Webhook.site to ensure formatting is correct
Deploy
❏ Replace the test URL with your production endpoint
❏ Add the timeout parameter
❏ Implement the error logging try...except block
Frequently Asked Questions
What's the difference between polling and webhooks?
Polling: External system asks Odoo every 5 minutes "Any updates?" (wasted API calls, 4:59 min delays, hits rate limits). Webhooks: Odoo pushes HTTP POST to external URL the instant event happens (200ms response, zero wasted calls, no rate limit issues).
Why is timeout=5 critical for webhooks?
If external API goes down and hangs (doesn't reject, just freezes), Odoo user clicks "Confirm" and spinner spins for 60 seconds. User thinks Odoo is broken. timeout=5 cuts connection after 5 seconds, logs error, doesn't block user. Never let external system failures freeze your Odoo.
When should I use Queue Jobs for webhooks?
Use Queue Jobs (OCA module) when: (1) Webhook might take long to process, (2) Sending hundreds at once, (3) Need automatic retries on failure. User clicks "Confirm," transaction saves instantly, webhook fires in background worker. If fails, Queue Job retries 3 times every 10 minutes automatically.
How do I secure webhooks from hackers?
Use shared secret in headers: Authorization: Bearer SECRET_TOKEN. Receiving server validates token before processing. Store token in Odoo System Parameters (not hardcoded). Without this, anyone can send fake "order confirmed" requests to your external system.
Free Integration Architecture Session
Stop relying on slow polling and expensive connector apps. We'll map out your data flow between Odoo and external apps, identify the critical events that need real-time syncing, provide the exact Python snippets for your specific webhooks, and review your security and timeout settings. Real-time data is the heartbeat of a modern D2C brand.
