Skip to content

Helpdesk API - Python Integration

Complete Python examples for integrating with the Helpdesk Ticket API.

Quick Start

Installation

pip install requests

Basic Setup

import json
import requests
from typing import Optional, List, Dict, Any

class OdooHelpdeskAPI:
    """Simple client for Odoo Helpdesk API."""

    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.headers = {
            'X-API-Key': api_key,
            # Note: Do NOT set Content-Type for POST requests
            # The REST API expects form data, not JSON
        }

    def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make API request and return JSON response."""
        url = f"{self.base_url}/restapi/1.0{endpoint}"
        response = requests.request(method, url, headers=self.headers, **kwargs)
        response.raise_for_status()
        return response.json()


# Initialize client
api = OdooHelpdeskAPI(
    base_url='https://your-odoo.com',
    api_key='your_api_key_here'
)

Important: The REST API expects form data with a vals parameter for POST/PUT requests, NOT JSON body. See the Complete Client Class below for proper implementation.

Complete Client Class

import json
import requests
from typing import Optional, List, Dict, Any
from datetime import datetime


class HelpdeskTicketClient:
    """
    Full-featured client for Odoo Helpdesk Ticket API.

    Usage:
        client = HelpdeskTicketClient('https://odoo.example.com', 'your_api_key')

        # Create ticket
        ticket = client.create_ticket(
            name='Issue with blinds',
            description='<p>Blinds not working properly</p>',
            partner_id=123,
            priority='2'
        )

        # Get ticket
        ticket = client.get_ticket(456)

        # Update ticket
        client.update_ticket(456, stage_id=3, priority='3')

        # Search tickets
        tickets = client.search_tickets(
            domain=[('priority', '=', '3')],
            limit=10
        )
    """

    def __init__(self, base_url: str, api_key: str, timeout: int = 30):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            'X-API-Key': api_key,
            # Note: Do NOT set Content-Type - REST API expects form data
        })

    def _endpoint(self, path: str) -> str:
        """Build full API endpoint URL."""
        return f"{self.base_url}/restapi/1.0{path}"

    def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict] = None,
        data: Optional[Dict] = None
    ) -> Dict[str, Any]:
        """
        Make API request with error handling.

        Args:
            method: HTTP method (GET, POST, PUT, DELETE)
            endpoint: API endpoint path
            params: Query parameters
            data: Data for POST/PUT (will be sent as form data with 'vals' param)

        Returns:
            Parsed JSON response

        Raises:
            HelpdeskAPIError: On API errors
        """
        url = self._endpoint(endpoint)

        # For POST/PUT, send data as form data with 'vals' parameter
        form_data = None
        if data and method in ('POST', 'PUT'):
            form_data = {'vals': json.dumps(data)}

        try:
            response = self.session.request(
                method=method,
                url=url,
                params=params,
                data=form_data,
                timeout=self.timeout
            )
            response.raise_for_status()
            return response.json()

        except requests.exceptions.HTTPError as e:
            error_data = {}
            try:
                error_data = e.response.json()
            except:
                pass
            raise HelpdeskAPIError(
                status_code=e.response.status_code,
                message=str(e),
                details=error_data
            )
        except requests.exceptions.RequestException as e:
            raise HelpdeskAPIError(
                status_code=0,
                message=f"Request failed: {str(e)}"
            )

    # -------------------------------------------------------------------------
    # Ticket CRUD Operations
    # -------------------------------------------------------------------------

    def create_ticket(
        self,
        name: str,
        description: Optional[str] = None,
        partner_id: Optional[int] = None,
        partner_name: Optional[str] = None,
        partner_email: Optional[str] = None,
        partner_phone: Optional[str] = None,
        team_id: Optional[int] = None,
        user_id: Optional[int] = None,
        ticket_type_id: Optional[int] = None,
        tag_ids: Optional[List[int]] = None,
        priority: str = '0',
        **kwargs
    ) -> Dict[str, Any]:
        """
        Create a new helpdesk ticket.

        Args:
            name: Ticket subject (required)
            description: HTML description
            partner_id: Customer ID
            partner_name: Customer name (if no partner_id)
            partner_email: Customer email
            partner_phone: Customer phone
            team_id: Helpdesk team ID
            user_id: Assigned user ID
            ticket_type_id: Ticket type ID
            tag_ids: List of tag IDs
            priority: '0' (All), '1' (Low), '2' (High), '3' (Urgent)
            **kwargs: Additional fields

        Returns:
            Created ticket data
        """
        data = {'name': name, 'priority': priority}

        if description:
            data['description'] = description
        if partner_id:
            data['partner_id'] = partner_id
        if partner_name:
            data['partner_name'] = partner_name
        if partner_email:
            data['partner_email'] = partner_email
        if partner_phone:
            data['partner_phone'] = partner_phone
        if team_id:
            data['team_id'] = team_id
        if user_id:
            data['user_id'] = user_id
        if ticket_type_id:
            data['ticket_type_id'] = ticket_type_id
        if tag_ids:
            data['tag_ids'] = [(6, 0, tag_ids)]  # Odoo many2many format

        data.update(kwargs)

        result = self._request('POST', '/object/helpdesk.ticket', data=data)
        return result.get('helpdesk.ticket', {})

    def get_ticket(
        self,
        ticket_id: int,
        fields: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        """
        Get a single ticket by ID.

        Args:
            ticket_id: Ticket ID
            fields: List of fields to return (None = all)

        Returns:
            Ticket data or None if not found
        """
        params = {}
        if fields:
            params['fields'] = str(fields)

        result = self._request(
            'GET',
            f'/object/helpdesk.ticket/{ticket_id}',
            params=params
        )

        tickets = result.get('helpdesk.ticket', [])
        return tickets[0] if tickets else None

    def update_ticket(
        self,
        ticket_id: int,
        **fields
    ) -> Dict[str, Any]:
        """
        Update a ticket.

        Args:
            ticket_id: Ticket ID
            **fields: Fields to update

        Returns:
            Updated ticket data
        """
        result = self._request(
            'PUT',
            f'/object/helpdesk.ticket/{ticket_id}',
            data=fields
        )
        return result.get('helpdesk.ticket', {})

    def delete_ticket(self, ticket_id: int) -> bool:
        """
        Delete a ticket.

        Args:
            ticket_id: Ticket ID

        Returns:
            True if successful
        """
        self._request('DELETE', f'/object/helpdesk.ticket/{ticket_id}')
        return True

    def search_tickets(
        self,
        domain: Optional[List] = None,
        fields: Optional[List[str]] = None,
        limit: int = 80,
        offset: int = 0,
        order: str = 'create_date desc'
    ) -> List[Dict[str, Any]]:
        """
        Search tickets with filters.

        Args:
            domain: Odoo domain filter (e.g., [('priority', '=', '3')])
            fields: Fields to return
            limit: Max records to return
            offset: Number of records to skip
            order: Sort order

        Returns:
            List of matching tickets
        """
        params = {
            'limit': limit,
            'offset': offset,
            'order': order
        }

        if domain:
            params['domain'] = str(domain)
        if fields:
            params['fields'] = str(fields)

        result = self._request('GET', '/object/helpdesk.ticket', params=params)
        return result.get('helpdesk.ticket', [])

    # -------------------------------------------------------------------------
    # Convenience Methods
    # -------------------------------------------------------------------------

    def get_open_tickets(
        self,
        limit: int = 80,
        fields: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Get all open (not closed) tickets."""
        return self.search_tickets(
            domain=[('stage_id.is_close', '=', False)],
            fields=fields,
            limit=limit
        )

    def get_urgent_tickets(
        self,
        limit: int = 80,
        fields: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Get all urgent priority tickets."""
        return self.search_tickets(
            domain=[('priority', '=', '3')],
            fields=fields,
            limit=limit
        )

    def get_unassigned_tickets(
        self,
        team_id: Optional[int] = None,
        limit: int = 80,
        fields: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Get unassigned tickets, optionally filtered by team."""
        domain = [('user_id', '=', False)]
        if team_id:
            domain.append(('team_id', '=', team_id))
        return self.search_tickets(domain=domain, fields=fields, limit=limit)

    def get_customer_tickets(
        self,
        partner_id: int,
        include_closed: bool = False,
        limit: int = 80,
        fields: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Get all tickets for a specific customer."""
        domain = [('partner_id', '=', partner_id)]
        if not include_closed:
            domain.append(('stage_id.is_close', '=', False))
        return self.search_tickets(domain=domain, fields=fields, limit=limit)

    def get_sla_failed_tickets(
        self,
        limit: int = 80,
        fields: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Get tickets that failed SLA."""
        return self.search_tickets(
            domain=[('sla_fail', '=', True)],
            fields=fields,
            limit=limit
        )

    def assign_ticket(self, ticket_id: int, user_id: int) -> Dict[str, Any]:
        """Assign a ticket to a user."""
        return self.update_ticket(ticket_id, user_id=user_id)

    def change_stage(self, ticket_id: int, stage_id: int) -> Dict[str, Any]:
        """Change ticket stage."""
        return self.update_ticket(ticket_id, stage_id=stage_id)

    def set_priority(self, ticket_id: int, priority: str) -> Dict[str, Any]:
        """
        Set ticket priority.

        Args:
            ticket_id: Ticket ID
            priority: '0' (All), '1' (Low), '2' (High), '3' (Urgent)
        """
        return self.update_ticket(ticket_id, priority=priority)

    # -------------------------------------------------------------------------
    # Related Models
    # -------------------------------------------------------------------------

    def get_teams(self) -> List[Dict[str, Any]]:
        """Get all helpdesk teams."""
        result = self._request(
            'GET',
            '/object/helpdesk.team',
            params={'fields': "['name', 'use_sla', 'member_ids']"}
        )
        return result.get('helpdesk.team', [])

    def get_stages(self, team_id: Optional[int] = None) -> List[Dict[str, Any]]:
        """Get helpdesk stages, optionally filtered by team."""
        params = {'fields': "['name', 'sequence', 'is_close', 'team_ids']"}
        if team_id:
            params['domain'] = str([('team_ids', 'in', [team_id])])

        result = self._request('GET', '/object/helpdesk.stage', params=params)
        return result.get('helpdesk.stage', [])

    def get_ticket_types(self) -> List[Dict[str, Any]]:
        """Get all ticket types."""
        result = self._request(
            'GET',
            '/object/helpdesk.ticket.type',
            params={'fields': "['name', 'sequence']"}
        )
        return result.get('helpdesk.ticket.type', [])

    def get_tags(self) -> List[Dict[str, Any]]:
        """Get all helpdesk tags."""
        result = self._request(
            'GET',
            '/object/helpdesk.tag',
            params={'fields': "['name', 'color']"}
        )
        return result.get('helpdesk.tag', [])


class HelpdeskAPIError(Exception):
    """Custom exception for API errors."""

    def __init__(
        self,
        status_code: int,
        message: str,
        details: Optional[Dict] = None
    ):
        self.status_code = status_code
        self.message = message
        self.details = details or {}
        super().__init__(f"[{status_code}] {message}")

Usage Examples

Create and Manage Tickets

# Initialize client
client = HelpdeskTicketClient(
    base_url='https://your-odoo.com',
    api_key='your_api_key_here'
)

# Create a new ticket
ticket = client.create_ticket(
    name='Motor not responding - Motorized blinds',
    description='<p>Customer reports the motor makes clicking sounds but blinds do not move.</p>',
    partner_id=123,
    team_id=1,
    priority='2',  # High
    ticket_type_id=1
)
print(f"Created ticket #{ticket['id']}: {ticket['name']}")

# Get ticket details
ticket = client.get_ticket(
    ticket_id=456,
    fields=['name', 'description', 'partner_id', 'stage_id', 'priority', 'user_id']
)
print(f"Ticket: {ticket['name']}")
print(f"Stage: {ticket['stage_id'][1]}")
print(f"Assigned to: {ticket['user_id'][1] if ticket['user_id'] else 'Unassigned'}")

# Update ticket
client.update_ticket(
    ticket_id=456,
    priority='3',  # Urgent
    stage_id=2     # In Progress
)

# Assign ticket
client.assign_ticket(ticket_id=456, user_id=10)

# Change priority
client.set_priority(ticket_id=456, priority='3')

Search and Filter Tickets

# Get all open urgent tickets
urgent_tickets = client.search_tickets(
    domain=[
        ('priority', '=', '3'),
        ('stage_id.is_close', '=', False)
    ],
    fields=['name', 'partner_id', 'create_date', 'sla_deadline'],
    limit=50,
    order='create_date desc'
)

for ticket in urgent_tickets:
    print(f"#{ticket['id']}: {ticket['name']}")
    print(f"  Customer: {ticket['partner_id'][1]}")
    print(f"  Created: {ticket['create_date']}")
    if ticket.get('sla_deadline'):
        print(f"  SLA Deadline: {ticket['sla_deadline']}")
    print()

# Get unassigned tickets
unassigned = client.get_unassigned_tickets(limit=20)
print(f"Found {len(unassigned)} unassigned tickets")

# Get customer tickets
customer_tickets = client.get_customer_tickets(
    partner_id=123,
    include_closed=False
)
print(f"Customer has {len(customer_tickets)} open tickets")

# Get SLA failed tickets
failed_sla = client.get_sla_failed_tickets()
print(f"{len(failed_sla)} tickets failed SLA")

Complex Domain Queries

# Tickets created in last 7 days with high/urgent priority
from datetime import datetime, timedelta

week_ago = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d 00:00:00')

recent_important = client.search_tickets(
    domain=[
        ('create_date', '>=', week_ago),
        '|',
        ('priority', '=', '2'),
        ('priority', '=', '3')
    ],
    fields=['name', 'priority', 'partner_id', 'stage_id'],
    order='priority desc, create_date desc'
)

# Tickets for specific email domain
company_tickets = client.search_tickets(
    domain=[('partner_email', 'ilike', '@bigcustomer.com')],
    fields=['name', 'partner_email', 'partner_id']
)

# Tickets assigned to specific users
team_tickets = client.search_tickets(
    domain=[('user_id', 'in', [5, 10, 15])],
    fields=['name', 'user_id', 'stage_id']
)

Batch Operations

# Process unassigned tickets
unassigned = client.get_unassigned_tickets(team_id=1)

# Auto-assign based on load (simple round-robin example)
team_members = [5, 10, 15]  # User IDs
for i, ticket in enumerate(unassigned):
    user_id = team_members[i % len(team_members)]
    client.assign_ticket(ticket['id'], user_id)
    print(f"Assigned ticket #{ticket['id']} to user {user_id}")

# Bulk priority update
tickets_to_update = client.search_tickets(
    domain=[
        ('priority', '=', '0'),
        ('create_date', '<=', week_ago)  # Old tickets with default priority
    ],
    fields=['id']
)

for ticket in tickets_to_update:
    client.set_priority(ticket['id'], '1')  # Set to Low

Error Handling

from requests.exceptions import RequestException

try:
    ticket = client.get_ticket(999999)  # Non-existent ID
    if ticket is None:
        print("Ticket not found")

except HelpdeskAPIError as e:
    print(f"API Error: {e.message}")
    print(f"Status Code: {e.status_code}")
    if e.details:
        print(f"Details: {e.details}")

except RequestException as e:
    print(f"Network error: {e}")

Working with Stages

# Get all stages for a team
stages = client.get_stages(team_id=1)
print("Available stages:")
for stage in sorted(stages, key=lambda s: s['sequence']):
    status = "(Closed)" if stage['is_close'] else ""
    print(f"  {stage['id']}: {stage['name']} {status}")

# Find the "Resolved" stage
resolved_stage = next(
    (s for s in stages if s['is_close']),
    None
)

# Close a ticket
if resolved_stage:
    client.change_stage(ticket_id=456, stage_id=resolved_stage['id'])
    print(f"Ticket closed with stage: {resolved_stage['name']}")

Webhook Integration Example

Example Flask webhook to receive external ticket requests:

from flask import Flask, request, jsonify

app = Flask(__name__)
client = HelpdeskTicketClient(
    base_url='https://your-odoo.com',
    api_key='your_api_key'
)

@app.route('/webhook/create-ticket', methods=['POST'])
def create_ticket_webhook():
    """Receive ticket creation requests from external systems."""
    data = request.json

    # Validate required fields
    if not data.get('subject'):
        return jsonify({'error': 'Subject is required'}), 400

    try:
        ticket = client.create_ticket(
            name=data['subject'],
            description=data.get('description', ''),
            partner_email=data.get('email'),
            partner_name=data.get('name'),
            partner_phone=data.get('phone'),
            priority=data.get('priority', '0')
        )

        return jsonify({
            'success': True,
            'ticket_id': ticket['id'],
            'ticket_name': ticket['name']
        })

    except HelpdeskAPIError as e:
        return jsonify({
            'success': False,
            'error': e.message
        }), 500

if __name__ == '__main__':
    app.run(port=5000)

Environment Configuration

Best practice for storing credentials:

import os
from dotenv import load_dotenv

load_dotenv()

client = HelpdeskTicketClient(
    base_url=os.getenv('ODOO_URL'),
    api_key=os.getenv('ODOO_API_KEY')
)

.env file:

ODOO_URL=https://your-odoo.com
ODOO_API_KEY=your_api_key_here