Odoo 18 Webhooks: Connect External Systems
By Braincuber Team
Published on February 4, 2026
Traditional API integrations rely on polling—your system repeatedly asks "anything new?" every few seconds. This wastes resources, creates delays, and puts unnecessary load on both systems. At CloudSync Solutions, we helped an e-commerce client reduce their integration overhead by 90% by switching from API polling to webhooks for their Stripe payment notifications.
Webhooks flip the model: instead of asking for updates, external systems push data to Odoo the moment something happens. A customer pays? Stripe instantly notifies Odoo. An order ships? Your logistics provider triggers Odoo in real-time. This guide walks through building webhook receivers in Odoo 18 using a practical Stripe payment integration example.
What You'll Build:
- Custom HTTP controller to receive webhook payloads
- Secure endpoint with signature verification
- Automatic payment confirmation in Odoo
- Error handling and logging for debugging
Webhooks vs API Polling: Understanding the Difference
Before diving into implementation, let's clarify why webhooks outperform traditional polling:
API Polling (Pull)
- Odoo asks Stripe: "Any new payments?"
- Repeats every 30 seconds (or more)
- Wastes API calls when nothing changed
- Delays: up to 30 seconds before detection
Webhooks (Push)
- Stripe tells Odoo: "Payment just happened!"
- Instant notification on each event
- Zero wasted calls—only real events
- Real-time: milliseconds after the event
Step 1: Create a Custom Odoo Module
Webhook receivers require a custom module with an HTTP controller. Start by creating the module structure:
# Module Structure
stripe_webhook/
├── __init__.py
├── __manifest__.py
├── controllers/
│ ├── __init__.py
│ └── main.py
└── models/
├── __init__.py
└── payment_transaction.py
Module Manifest
# __manifest__.py
{
'name': 'Stripe Webhook Integration',
'version': '18.0.1.0.0',
'category': 'Accounting/Payment',
'summary': 'Receive Stripe payment webhooks in real-time',
'description': '''
This module creates webhook endpoints to receive
payment notifications from Stripe automatically.
''',
'author': 'Your Company',
'depends': ['payment', 'account'],
'data': [],
'installable': True,
'application': False,
'license': 'LGPL-3',
}
Step 2: Build the Webhook Controller
The controller defines the HTTP endpoint that receives incoming webhook requests. This is where Stripe sends payment event data.
# controllers/main.py
import json
import logging
import hmac
import hashlib
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class StripeWebhookController(http.Controller):
@http.route('/webhook/stripe/payment',
type='json',
auth='none',
methods=['POST'],
csrf=False)
def handle_stripe_webhook(self, **kwargs):
"""
Handle incoming Stripe payment webhooks.
Endpoint: https://your-odoo.com/webhook/stripe/payment
"""
try:
# Get the raw payload
payload = request.get_json_data()
# Log the incoming webhook
_logger.info("Stripe Webhook Received: %s",
payload.get('type', 'unknown'))
# Route to appropriate handler based on event type
event_type = payload.get('type', '')
if event_type == 'payment_intent.succeeded':
return self._handle_payment_success(payload)
elif event_type == 'payment_intent.payment_failed':
return self._handle_payment_failure(payload)
elif event_type == 'charge.refunded':
return self._handle_refund(payload)
else:
_logger.info("Unhandled event type: %s", event_type)
return {'status': 'ignored', 'event': event_type}
except Exception as e:
_logger.error("Webhook processing error: %s", str(e))
return {'status': 'error', 'message': str(e)}
def _handle_payment_success(self, payload):
"""Process successful payment notification."""
data = payload.get('data', {}).get('object', {})
payment_intent_id = data.get('id')
amount = data.get('amount', 0) / 100 # Convert cents to dollars
currency = data.get('currency', 'usd').upper()
customer_email = data.get('receipt_email')
_logger.info("Payment Success: %s - %s %s",
payment_intent_id, amount, currency)
# Find and update the corresponding Odoo transaction
# Your business logic here...
return {
'status': 'success',
'payment_id': payment_intent_id,
'amount': amount
}
def _handle_payment_failure(self, payload):
"""Process failed payment notification."""
data = payload.get('data', {}).get('object', {})
payment_intent_id = data.get('id')
error = data.get('last_payment_error', {})
_logger.warning("Payment Failed: %s - %s",
payment_intent_id,
error.get('message', 'Unknown error'))
return {'status': 'recorded', 'payment_id': payment_intent_id}
def _handle_refund(self, payload):
"""Process refund notification."""
data = payload.get('data', {}).get('object', {})
charge_id = data.get('id')
refund_amount = data.get('amount_refunded', 0) / 100
_logger.info("Refund Processed: %s - $%s", charge_id, refund_amount)
return {'status': 'refund_recorded', 'charge_id': charge_id}
Security Warning: Using auth='none' makes this endpoint public. Always implement signature verification (shown below) for production deployments.
Step 3: Implement Webhook Signature Verification
Public endpoints are vulnerable to spoofed requests. Stripe signs every webhook with a secret, allowing you to verify authenticity.
# Add to controllers/main.py
import time
STRIPE_WEBHOOK_SECRET = 'whsec_your_webhook_signing_secret'
def verify_stripe_signature(self, payload, sig_header):
"""
Verify that the webhook came from Stripe.
Returns True if valid, False otherwise.
"""
try:
# Parse the signature header
# Format: t=timestamp,v1=signature
elements = dict(item.split('=') for item in sig_header.split(','))
timestamp = elements.get('t')
signature = elements.get('v1')
if not timestamp or not signature:
return False
# Check timestamp to prevent replay attacks (5 min tolerance)
if abs(time.time() - int(timestamp)) > 300:
_logger.warning("Webhook timestamp too old")
return False
# Compute expected signature
signed_payload = f"{timestamp}.{json.dumps(payload)}"
expected_sig = hmac.new(
STRIPE_WEBHOOK_SECRET.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(signature, expected_sig)
except Exception as e:
_logger.error("Signature verification failed: %s", str(e))
return False
# Update the main handler to verify signatures
@http.route('/webhook/stripe/payment',
type='json',
auth='none',
methods=['POST'],
csrf=False)
def handle_stripe_webhook(self, **kwargs):
"""Handle incoming Stripe payment webhooks with verification."""
try:
payload = request.get_json_data()
sig_header = request.httprequest.headers.get('Stripe-Signature', '')
# Verify signature before processing
if not self.verify_stripe_signature(payload, sig_header):
_logger.warning("Invalid webhook signature rejected")
return {'status': 'error', 'message': 'Invalid signature'}
# Continue with normal processing...
event_type = payload.get('type', '')
# ... rest of handler
except Exception as e:
_logger.error("Webhook error: %s", str(e))
return {'status': 'error', 'message': str(e)}
Step 4: Configure Webhooks in Stripe Dashboard
With your Odoo endpoint ready, configure Stripe to send events to it:
- Access Stripe Dashboard: Log in to
dashboard.stripe.com - Navigate to Webhooks: Go to
Developers→Webhooks - Add Endpoint: Click
Add endpoint - Configure Endpoint:
- Endpoint URL: https://your-odoo-domain.com/webhook/stripe/payment
- Events: Select events to receive (payment_intent.succeeded, charge.refunded, etc.)
- Copy Signing Secret: After saving, copy the
whsec_...secret for signature verification
| Event Type | When It Fires | Common Use Case |
|---|---|---|
| payment_intent.succeeded | Payment completes successfully | Confirm orders, send receipts |
| payment_intent.payment_failed | Payment attempt fails | Notify customer, retry logic |
| charge.refunded | Refund is processed | Update order status, accounting |
| customer.subscription.created | New subscription starts | Provision access, welcome email |
| invoice.payment_failed | Subscription payment fails | Dunning emails, suspend access |
Step 5: Sample Webhook Payload
Understanding the payload structure helps you extract the right data. Here's what Stripe sends for a successful payment:
{
"id": "evt_1234567890",
"type": "payment_intent.succeeded",
"created": 1709654321,
"data": {
"object": {
"id": "pi_3OxYz123456789",
"object": "payment_intent",
"amount": 9999,
"currency": "usd",
"status": "succeeded",
"receipt_email": "customer@example.com",
"metadata": {
"order_id": "SO-2024-00142",
"customer_name": "John Smith"
},
"payment_method": "pm_card_visa",
"created": 1709654300
}
},
"livemode": true
}
Pro Tip: Use metadata when creating payments on Stripe to include your Odoo order ID. This makes matching webhook events to Odoo records trivial.
Step 6: Testing Webhooks Locally
External services like Stripe can't reach your localhost. Use tunneling tools to expose your local Odoo for testing:
Using ngrok
# Install ngrok
npm install -g ngrok
# Start your local Odoo (default port 8069)
./odoo-bin -c odoo.conf
# In another terminal, create tunnel
ngrok http 8069
# Output:
# Forwarding: https://abc123.ngrok.io -> http://localhost:8069
# Use this URL in Stripe:
# https://abc123.ngrok.io/webhook/stripe/payment
Using Stripe CLI
# Install Stripe CLI
# macOS: brew install stripe/stripe-cli/stripe
# Windows: scoop install stripe
# Login to Stripe
stripe login
# Forward webhooks to local endpoint
stripe listen --forward-to localhost:8069/webhook/stripe/payment
# Trigger test events
stripe trigger payment_intent.succeeded
Production Deployment Checklist
Before going live, ensure your webhook implementation meets these requirements:
HTTPS Required
All webhook endpoints must use HTTPS with valid SSL certificates. HTTP endpoints are rejected by most providers.
Signature Verification
Never process webhooks without verifying the cryptographic signature. Spoofed webhooks can corrupt your data.
Idempotency Handling
Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing.
Respond Quickly
Return HTTP 200 within 5-10 seconds. Process heavy work asynchronously with queued jobs.
Comprehensive Logging
Log all incoming webhooks with timestamps. Essential for debugging and audit trails.
Frequently Asked Questions
Conclusion
Webhooks transform Odoo from an isolated system into a real-time integration hub. Instead of constantly polling external services for updates, your Odoo instance receives instant notifications the moment events occur—payments confirmed, orders placed, inventory updated, subscriptions changed.
The implementation pattern shown here—custom HTTP controller, signature verification, event routing, and proper logging—applies to any webhook provider: Stripe, Shopify, WooCommerce, Slack, GitHub, or custom internal systems. Master this pattern once, and you can integrate Odoo with virtually any modern platform that supports webhooks.
Need Help Building Odoo Integrations?
Our development team specializes in connecting Odoo with external platforms. We can build custom webhook receivers, implement secure integrations, set up real-time synchronization, and ensure your systems communicate reliably.
