What Are Webhooks and Why Does Odoo Need Them?
Webhooks are HTTP callbacks — when something happens in an external service (a payment succeeds in Stripe, an order is placed on Shopify, a commit is pushed to GitHub), that service sends an HTTP POST request to your Odoo URL with the event data. Instead of polling APIs every few minutes asking "did anything change?", webhooks push changes to you in real time. This is faster, more efficient, and more reliable.
How Webhooks Work with Odoo
The flow is straightforward:
- External service triggers an event (e.g., payment completed)
- External service sends HTTP POST to your Odoo webhook URL
- Odoo controller receives the request, validates it, and processes the data
- Odoo creates or updates records based on the webhook payload
- Odoo returns a 200 status to confirm receipt
Step 1: Create the Webhook Controller
import json
import logging
from odoo import http
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
class WebhookController(http.Controller):
@http.route('/webhook/incoming', type='http', auth='none',
methods=['POST'], csrf=False, save_session=False)
def receive_webhook(self, **kwargs):
try:
# Parse the payload
body = request.httprequest.get_data(as_text=True)
payload = json.loads(body)
# Validate the webhook (see Step 2)
if not self._validate_signature(request.httprequest):
_logger.warning('Webhook signature validation failed')
return Response('Unauthorized', status=401)
# Process asynchronously (see Step 3)
self._process_webhook(payload)
return Response('OK', status=200)
except json.JSONDecodeError:
_logger.error('Webhook received invalid JSON')
return Response('Bad Request', status=400)
except Exception as e:
_logger.exception('Webhook processing error: %s', e)
return Response('Internal Server Error', status=500)Step 2: Validate Webhook Signatures
Never trust incoming webhooks without validation. Most services sign their payloads with a shared secret:
HMAC-SHA256 Validation (Stripe, GitHub, Shopify)
import hmac
import hashlib
def _validate_signature(self, httprequest):
secret = request.env['ir.config_parameter'].sudo().get_param(
'webhook_secret', ''
)
if not secret:
_logger.error('Webhook secret not configured')
return False
# Get the signature header (varies by service)
# Stripe: Stripe-Signature
# GitHub: X-Hub-Signature-256
# Shopify: X-Shopify-Hmac-SHA256
signature = httprequest.headers.get('X-Webhook-Signature', '')
body = httprequest.get_data()
expected = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Step 3: Process Webhooks Safely
Webhook processing should be fast and idempotent. The external service expects a response within 5-30 seconds and will retry if it does not get one.
Pattern: Acknowledge First, Process Later
def receive_webhook(self, **kwargs):
body = request.httprequest.get_data(as_text=True)
payload = json.loads(body)
if not self._validate_signature(request.httprequest):
return Response('Unauthorized', status=401)
# Store the raw webhook for processing
request.env['webhook.queue'].sudo().create({
'payload': body,
'source': 'stripe',
'event_type': payload.get('type', 'unknown'),
'state': 'pending',
})
# Return immediately
return Response('OK', status=200)
# Cron job processes the queue
def _process_webhook_queue(self):
pending = self.search([('state', '=', 'pending')], limit=50)
for webhook in pending:
try:
payload = json.loads(webhook.payload)
self._handle_event(payload)
webhook.state = 'processed'
except Exception as e:
webhook.state = 'failed'
webhook.error_message = str(e)Step 4: Handle Different Event Types
Route events to the appropriate handler based on the event type:
def _handle_event(self, payload):
event_type = payload.get('type', '')
handlers = {
'payment.completed': self._handle_payment_completed,
'payment.failed': self._handle_payment_failed,
'order.created': self._handle_order_created,
'customer.updated': self._handle_customer_updated,
'refund.processed': self._handle_refund,
}
handler = handlers.get(event_type)
if handler:
handler(payload)
else:
_logger.info('Unhandled webhook event type: %s', event_type)Step 5: Make Processing Idempotent
Webhook services retry delivery on failure. Your handler must handle duplicate deliveries gracefully:
def _handle_payment_completed(self, payload):
payment_id = payload['data']['id']
# Check if already processed
existing = self.env['account.payment'].sudo().search([
('external_ref', '=', payment_id)
])
if existing:
_logger.info('Payment %s already processed, skipping', payment_id)
return
# Process the payment
self.env['account.payment'].sudo().create({
'external_ref': payment_id,
'amount': payload['data']['amount'] / 100,
'partner_id': self._find_partner(payload['data']['customer']).id,
# ... other fields
})Step 6: Register Your Webhook URL
Configure the external service to send webhooks to your Odoo endpoint:
| Service | Webhook Configuration Location | URL Format |
|---|---|---|
| Stripe | Dashboard → Developers → Webhooks | /webhook/stripe |
| Shopify | Settings → Notifications → Webhooks | /webhook/shopify |
| GitHub | Repo → Settings → Webhooks | /webhook/github |
| PayPal | Developer Dashboard → Webhooks | /webhook/paypal |
| Mailchimp | Audience → Settings → Webhooks | /webhook/mailchimp |
Troubleshooting
Webhook Returns 404
The module containing the controller must be installed. Odoo only registers routes from installed modules. Also verify the route path matches exactly — Odoo routes are case-sensitive and do not handle trailing slashes automatically.
CSRF Error on Webhook Endpoint
Set csrf=False on the route decorator. Webhooks are machine-to-machine calls that do not carry CSRF tokens. Use signature validation instead for security.
Webhook Timeout (Service Reports Failure)
Your controller must respond within the service's timeout (typically 5-30 seconds). If processing takes longer, use the queue pattern: store the payload, return 200 immediately, and process via a cron job. This also prevents data loss if processing fails.
Payload Parsing Error
Some services send application/x-www-form-urlencoded instead of JSON. Check the Content-Type header and parse accordingly. Use type='http' (not type='json') on the route for maximum flexibility in handling different content types.
Security Best Practices
- Always validate signatures — never process unsigned webhooks in production
- Use HTTPS — webhook payloads may contain sensitive data
- Store secrets securely — use ir.config_parameter, not hardcoded values
- Rate limit — protect against webhook flooding
- Log everything — log received webhooks for debugging and audit
- Whitelist IPs — some services publish their webhook source IPs
DeployMonkey Webhook Support
DeployMonkey instances have stable HTTPS URLs that work perfectly for webhook endpoints. Our AI agent can generate webhook handler code for any external service, set up signature validation, and troubleshoot delivery issues — just describe what service you want to receive webhooks from.