Skip to content

How to Receive Webhooks in Odoo: Complete Integration Guide

DeployMonkey Team · March 23, 2026 12 min read

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:

  1. External service triggers an event (e.g., payment completed)
  2. External service sends HTTP POST to your Odoo webhook URL
  3. Odoo controller receives the request, validates it, and processes the data
  4. Odoo creates or updates records based on the webhook payload
  5. 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:

ServiceWebhook Configuration LocationURL Format
StripeDashboard → Developers → Webhooks/webhook/stripe
ShopifySettings → Notifications → Webhooks/webhook/shopify
GitHubRepo → Settings → Webhooks/webhook/github
PayPalDeveloper Dashboard → Webhooks/webhook/paypal
MailchimpAudience → 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.