Helpdesk API - Python Integration¶
Complete Python examples for integrating with the Helpdesk Ticket API.
Quick Start¶
Installation¶
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: