October 22, 2025

Integrating Odoo with External Systems via REST APIs

By CleverBusiness Engineering Team

Odoo is powerful, but modern businesses need integrations. Here's our approach to connecting Odoo with payment processors, shipping providers, CRM systems, and custom applications using REST APIs and webhooks.

The Integration Challenge

Out of the box, Odoo provides XML-RPC and JSON-RPC APIs. While functional, they have limitations:

  • Not RESTful: External developers expect REST conventions (GET, POST, PUT, DELETE)
  • Authentication complexity: XML-RPC sessions are cumbersome for modern apps
  • No webhooks: Can't push real-time events to external systems
  • Limited filtering: Domain filters are Odoo-specific syntax

Our REST API Layer

Architecture Overview

External System → API Gateway → REST Controller → Odoo Models → PostgreSQL
                     ↑                                    ↓
                     └──────────── Webhooks ──────────────┘

1. RESTful Controllers

We built custom Odoo controllers that expose RESTful endpoints:

from odoo import http
from odoo.http import request
import json

class RentalAPI(http.Controller):

    @http.route('/api/v1/rentals', type='http', auth='api_key',
                methods=['GET'], csrf=False)
    def get_rentals(self, **params):
        """Get rental reservations with filtering"""

        # Parse query parameters
        customer_id = params.get('customer_id')
        status = params.get('status')
        start_date = params.get('start_date')
        limit = int(params.get('limit', 100))
        offset = int(params.get('offset', 0))

        # Build domain
        domain = []
        if customer_id:
            domain.append(('customer_id', '=', int(customer_id)))
        if status:
            domain.append(('state', '=', status))
        if start_date:
            domain.append(('start_date', '>=', start_date))

        # Query Odoo
        Rental = request.env['rental.reservation'].sudo()
        rentals = Rental.search(domain, limit=limit, offset=offset)
        total = Rental.search_count(domain)

        # Serialize response
        return request.make_response(
            json.dumps({
                'data': [r._to_json() for r in rentals],
                'total': total,
                'limit': limit,
                'offset': offset
            }),
            headers={'Content-Type': 'application/json'}
        )

    @http.route('/api/v1/rentals', type='json', auth='api_key',
                methods=['POST'], csrf=False)
    def create_rental(self, **data):
        """Create new rental reservation"""

        try:
            Rental = request.env['rental.reservation'].sudo()
            rental = Rental.create({
                'customer_id': data['customer_id'],
                'product_id': data['product_id'],
                'start_date': data['start_date'],
                'end_date': data['end_date'],
                'pickup_location': data.get('pickup_location'),
                'return_location': data.get('return_location')
            })

            return {
                'success': True,
                'rental_id': rental.id,
                'rental': rental._to_json()
            }

        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }

2. API Key Authentication

We implemented token-based authentication instead of username/password:

class APIAuth(http.Controller):

    def _authenticate_api_key(self, api_key):
        """Validate API key and return user"""
        APIKey = request.env['api.key'].sudo()
        key_record = APIKey.search([
            ('key', '=', api_key),
            ('active', '=', True),
            ('expiry_date', '>=', fields.Date.today())
        ], limit=1)

        if not key_record:
            raise werkzeug.exceptions.Unauthorized('Invalid API key')

        # Update last used
        key_record.last_used = fields.Datetime.now()

        return key_record.user_id

# Custom auth method
def api_key_auth(self):
    api_key = request.httprequest.headers.get('X-API-Key')
    if not api_key:
        raise werkzeug.exceptions.Unauthorized('API key required')

    user = self._authenticate_api_key(api_key)
    request.uid = user.id

3. Webhooks for Real-Time Events

When events occur in Odoo, we push notifications to external systems:

class RentalReservation(models.Model):
    _inherit = 'rental.reservation'

    @api.model
    def create(self, vals):
        """Trigger webhook on new reservation"""
        rental = super().create(vals)

        # Send webhook
        self._trigger_webhook('rental.created', {
            'rental_id': rental.id,
            'customer_id': rental.customer_id.id,
            'product_id': rental.product_id.id,
            'start_date': rental.start_date.isoformat(),
            'end_date': rental.end_date.isoformat(),
            'total_amount': rental.total_amount
        })

        return rental

    def write(self, vals):
        """Trigger webhook on status change"""
        old_state = self.state
        result = super().write(vals)

        if 'state' in vals and vals['state'] != old_state:
            self._trigger_webhook('rental.status_changed', {
                'rental_id': self.id,
                'old_status': old_state,
                'new_status': self.state,
                'timestamp': fields.Datetime.now().isoformat()
            })

        return result

    def _trigger_webhook(self, event_type, data):
        """Send webhook to registered endpoints"""
        Webhook = self.env['webhook.subscription'].sudo()
        subscriptions = Webhook.search([
            ('event_type', '=', event_type),
            ('active', '=', True)
        ])

        for subscription in subscriptions:
            # Async webhook delivery
            self.env['webhook.delivery'].create({
                'subscription_id': subscription.id,
                'event_type': event_type,
                'payload': json.dumps(data),
                'target_url': subscription.url
            }).send_async()

Real-World Integrations

Payment Processing (Stripe)

Synchronize rental invoices with Stripe for payment collection:

class AccountMove(models.Model):
    _inherit = 'account.move'

    stripe_invoice_id = fields.Char('Stripe Invoice ID')
    stripe_payment_intent = fields.Char('Stripe Payment Intent')

    def action_post(self):
        """Create Stripe invoice when Odoo invoice is posted"""
        result = super().action_post()

        for invoice in self:
            if invoice.move_type == 'out_invoice' and not invoice.stripe_invoice_id:
                # Create Stripe invoice
                stripe_invoice = stripe.Invoice.create(
                    customer=invoice.partner_id.stripe_customer_id,
                    description=f'Rental Invoice {invoice.name}',
                    metadata={'odoo_invoice_id': invoice.id}
                )

                # Add line items
                for line in invoice.invoice_line_ids:
                    stripe.InvoiceItem.create(
                        invoice=stripe_invoice.id,
                        customer=invoice.partner_id.stripe_customer_id,
                        amount=int(line.price_total * 100),  # cents
                        currency='usd',
                        description=line.name
                    )

                # Finalize and send
                stripe.Invoice.finalize_invoice(stripe_invoice.id)
                invoice.stripe_invoice_id = stripe_invoice.id

        return result

Shipping Integration (ShipStation)

Auto-create shipping labels for rental equipment delivery:

class StockPicking(models.Model):
    _inherit = 'stock.picking'

    shipstation_order_id = fields.Char('ShipStation Order ID')
    tracking_number = fields.Char('Tracking Number')

    def action_confirm(self):
        """Create ShipStation order when delivery is confirmed"""
        result = super().action_confirm()

        for picking in self:
            if picking.picking_type_code == 'outgoing' and not picking.shipstation_order_id:
                # Create ShipStation order
                response = requests.post(
                    'https://ssapi.shipstation.com/orders/createorder',
                    auth=(API_KEY, API_SECRET),
                    json={
                        'orderNumber': picking.name,
                        'orderDate': picking.scheduled_date.isoformat(),
                        'orderStatus': 'awaiting_shipment',
                        'customerEmail': picking.partner_id.email,
                        'shipTo': {
                            'name': picking.partner_id.name,
                            'street1': picking.partner_id.street,
                            'city': picking.partner_id.city,
                            'state': picking.partner_id.state_id.code,
                            'postalCode': picking.partner_id.zip,
                            'country': picking.partner_id.country_id.code
                        },
                        'items': [{
                            'sku': move.product_id.default_code,
                            'name': move.product_id.name,
                            'quantity': move.product_uom_qty
                        } for move in picking.move_ids_without_package]
                    }
                )

                if response.status_code == 200:
                    picking.shipstation_order_id = response.json()['orderId']

        return result

CRM Integration (HubSpot)

Sync customer data and rental activity to HubSpot:

class ResPartner(models.Model):
    _inherit = 'res.partner'

    hubspot_contact_id = fields.Char('HubSpot Contact ID')

    @api.model
    def create(self, vals):
        """Create HubSpot contact when customer is created"""
        partner = super().create(vals)

        if partner.customer_rank > 0:
            # Create HubSpot contact
            response = requests.post(
                'https://api.hubapi.com/contacts/v1/contact',
                headers={'Authorization': f'Bearer {HUBSPOT_API_KEY}'},
                json={
                    'properties': [
                        {'property': 'email', 'value': partner.email},
                        {'property': 'firstname', 'value': partner.name.split()[0]},
                        {'property': 'lastname', 'value': ' '.join(partner.name.split()[1:])},
                        {'property': 'phone', 'value': partner.phone},
                        {'property': 'odoo_customer_id', 'value': str(partner.id)}
                    ]
                }
            )

            if response.status_code == 200:
                partner.hubspot_contact_id = response.json()['vid']

        return partner

Performance Considerations

Rate Limiting

from odoo.addons.web.controllers.main import ratelimit

@http.route('/api/v1/rentals', auth='api_key')
@ratelimit(limit=1000, interval=3600)  # 1000 req/hour
def get_rentals(self, **params):
    # ... implementation

Caching

from werkzeug.contrib.cache import RedisCache
cache = RedisCache(host='localhost', port=6379)

def get_rentals(self, **params):
    cache_key = f"rentals:{customer_id}:{status}"
    cached = cache.get(cache_key)

    if cached:
        return cached

    # Query database
    result = # ...
    cache.set(cache_key, result, timeout=300)  # 5 min
    return result

API Documentation

We auto-generate OpenAPI (Swagger) documentation:

openapi: 3.0.0
info:
  title: CleverBusiness Rental API
  version: 1.0.0
paths:
  /api/v1/rentals:
    get:
      summary: List rental reservations
      parameters:
        - name: customer_id
          in: query
          schema:
            type: integer
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, confirmed, picked_up, returned]
      security:
        - ApiKeyAuth: []
      responses:
        200:
          description: List of rentals
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Rental'

Monitoring & Logging

Track API usage and errors with custom logging:

class APILog(models.Model):
    _name = 'api.log'

    timestamp = fields.Datetime(default=fields.Datetime.now)
    endpoint = fields.Char()
    method = fields.Char()
    api_key_id = fields.Many2one('api.key')
    status_code = fields.Integer()
    response_time = fields.Float()  # milliseconds
    error_message = fields.Text()

# Middleware logging
def log_api_request(endpoint, method, start_time, status, error=None):
    request.env['api.log'].sudo().create({
        'endpoint': endpoint,
        'method': method,
        'api_key_id': request.api_key_id,
        'status_code': status,
        'response_time': (time.time() - start_time) * 1000,
        'error_message': error
    })

Results

Since implementing our REST API layer:

  • 12 external integrations connected (Stripe, ShipStation, HubSpot, Zapier, custom apps)
  • 450K+ API calls/month processed
  • avg 85ms response time
  • 99.97% uptime for API endpoints
  • Zero manual data entry between systems

Best Practices

1. Version your APIs: Use /api/v1/ prefix to allow backwards-compatible changes.

2. Use sudo() carefully: Always validate permissions even with API keys.

3. Async webhook delivery: Don't block requests waiting for external systems.

4. Comprehensive logging: Track every API call for debugging and analytics.

Need API integrations for your Odoo instance? Contact us to discuss your requirements.