Skip to main content

Overview

Frappe Helpdesk provides a robust backend API system built on the Frappe Framework. You can create custom API endpoints to extend functionality and integrate with external systems.

API Structure

All API endpoints are located in helpdesk/api/ directory. Each module focuses on a specific domain (tickets, agents, contacts, etc.).

Basic API Endpoint

Create a new Python file in helpdesk/api/ or add to an existing one:
import frappe
from frappe import _

@frappe.whitelist()
def my_custom_endpoint(param1: str, param2: int) -> dict:
    """
    Custom API endpoint example.
    
    :param param1: Description of parameter 1
    :param param2: Description of parameter 2
    :return: Response data
    """
    # Your logic here
    result = {
        "success": True,
        "data": f"Processed {param1} with {param2}"
    }
    return result
Key points:
  • Use @frappe.whitelist() decorator to expose the function as an API endpoint
  • Add type hints for better code quality (required by require_type_annotated_api_methods = True in hooks.py)
  • Include docstrings for documentation

Real-World Examples

Example 1: Simple Action API (from agent.py)

import frappe
from helpdesk.utils import agent_only

@frappe.whitelist()
@agent_only
def sent_invites(emails: list[str], send_welcome_mail_to_user: bool = True):
    for email in emails:
        if frappe.db.exists("User", email):
            user = frappe.get_doc("User", email)
        else:
            user = frappe.get_doc(
                {"doctype": "User", "email": email, "first_name": email.split("@")[0]}
            ).insert()

            if send_welcome_mail_to_user:
                user.send_welcome_mail_to_user()

        frappe.get_doc(
            {
                "doctype": "HD Agent",
                "ID": email,
                "user": user.name,
                "agent_name": user.full_name,
                "user_image": user.user_image,
            }
        ).insert()
    return

Example 2: Document Manipulation API (from ticket.py)

import frappe
from frappe import _

def assign_ticket_to_agent(ticket_id, agent_id=None):
    if not ticket_id:
        return

    ticket_doc = frappe.get_doc("HD Ticket", ticket_id)

    if not agent_id:
        # assign to self
        agent_id = frappe.session.user

    if not frappe.db.exists("HD Agent", agent_id):
        frappe.throw(_("Tickets can only be assigned to agents"))

    ticket_doc.assign_agent(agent_id)
    return ticket_doc

Example 3: Complex List API (from doc.py)

import frappe
from frappe.model.document import get_controller

@frappe.whitelist()
def get_list_data(
    doctype: str,
    filters: dict = {},
    default_filters: dict = {},
    order_by: str = "modified desc",
    page_length: int = 20,
    columns: list = [],
    rows: list = [],
) -> dict:
    rows = frappe.parse_json(rows or "[]")
    columns = frappe.parse_json(columns or "[]")
    filters = frappe.parse_json(filters or "[]")

    if not rows:
        rows = ["name"]

    data = (
        frappe.get_list(
            doctype,
            fields=rows,
            filters=filters,
            order_by=order_by,
            page_length=page_length,
        )
        or []
    )

    return {
        "data": data,
        "columns": columns,
        "rows": rows,
        "total_count": frappe.get_list(doctype, fields=["count(*) as count"], filters=filters)[0].get("count", 0),
        "row_count": len(data),
    }

Using Query Builder

Always prefer frappe.qb.get_query() over frappe.db.get_all() for new code:
import frappe
from frappe.query_builder import DocType

@frappe.whitelist()
def get_open_tickets_for_agent(agent_id: str) -> list:
    Ticket = DocType("HD Ticket")
    
    query = (
        frappe.qb.from_(Ticket)
        .select(
            Ticket.name,
            Ticket.subject,
            Ticket.status,
            Ticket.priority,
            Ticket.modified
        )
        .where(Ticket._assign.like(f"%{agent_id}%"))
        .where(Ticket.status != "Closed")
        .orderby(Ticket.modified, order=frappe.qb.desc)
        .limit(100)
    )
    
    return query.run(as_dict=True)

Permission Utilities

Using Custom Decorators

from helpdesk.utils import agent_only

@frappe.whitelist()
@agent_only
def agent_specific_action():
    """Only agents can access this endpoint"""
    pass

Manual Permission Checks

from helpdesk.utils import check_permissions, is_agent, is_admin

@frappe.whitelist()
def check_user_access(doctype: str, name: str):
    # Check if user has read permission
    check_permissions(doctype, None)
    
    # Check if user is an agent
    if not is_agent():
        frappe.throw(_("Only agents can access this resource"))
    
    # Your logic here
    return frappe.get_doc(doctype, name)

Error Handling

Use Frappe’s built-in error handling:
import frappe
from frappe import _

@frappe.whitelist()
def validate_and_process(ticket_id: str, status: str):
    # Validation errors
    if not ticket_id:
        frappe.throw(_("Ticket ID is required"))
    
    if status not in ["Open", "Closed", "Replied"]:
        frappe.throw(_("Invalid status value"))
    
    # Check existence
    if not frappe.db.exists("HD Ticket", ticket_id):
        frappe.throw(_("Ticket {0} does not exist").format(ticket_id))
    
    # Permission errors
    if not frappe.has_permission("HD Ticket", "write", ticket_id):
        frappe.throw(_("Insufficient permissions"), frappe.PermissionError)
    
    try:
        ticket = frappe.get_doc("HD Ticket", ticket_id)
        ticket.status = status
        ticket.save()
        return {"success": True}
    except Exception as e:
        frappe.log_error(title=f"Error updating ticket {ticket_id}")
        frappe.throw(_("Failed to update ticket: {0}").format(str(e)))

Real-time Updates

Publish real-time events to connected clients:
from helpdesk.utils import publish_event, get_doc_room

@frappe.whitelist()
def update_ticket_status(ticket_id: str, status: str):
    ticket = frappe.get_doc("HD Ticket", ticket_id)
    ticket.status = status
    ticket.save()
    
    # Publish update to all users viewing this ticket
    room = get_doc_room("HD Ticket", ticket_id)
    publish_event(
        "helpdesk:ticket-update",
        room=room,
        data={"ticket_id": ticket_id, "status": status}
    )
    
    return {"success": True}

Database Operations

Create Document

@frappe.whitelist()
def create_ticket(subject: str, description: str, raised_by: str) -> str:
    ticket = frappe.get_doc({
        "doctype": "HD Ticket",
        "subject": subject,
        "description": description,
        "raised_by": raised_by,
    })
    ticket.insert()
    return ticket.name

Update Document

@frappe.whitelist()
def update_ticket_priority(ticket_id: str, priority: str):
    ticket = frappe.get_doc("HD Ticket", ticket_id)
    ticket.priority = priority
    ticket.save()
    return ticket

Delete Document

@frappe.whitelist()
def delete_ticket(ticket_id: str):
    frappe.delete_doc("HD Ticket", ticket_id)
    return {"success": True}

Bulk Operations

@frappe.whitelist()
def bulk_assign_tickets(ticket_ids: list[str], agent_id: str):
    ticket_ids = frappe.parse_json(ticket_ids)
    
    for ticket_id in ticket_ids:
        ticket = frappe.get_doc("HD Ticket", ticket_id)
        ticket.assign_agent(agent_id)
    
    frappe.db.commit()
    return {"success": True, "count": len(ticket_ids)}

Caching

Use Redis caching for expensive operations:
from frappe.utils.caching import redis_cache

@frappe.whitelist()
@redis_cache()
def get_filterable_fields(doctype: str) -> list:
    # Expensive operation cached in Redis
    fields = frappe.get_meta(doctype).fields
    return [{
        "label": field.label,
        "fieldname": field.fieldname,
        "fieldtype": field.fieldtype,
    } for field in fields]

Testing Your API

Test API endpoints using the Frappe console:
bench --site helpdesk.test console
Then in the console:
import frappe
frappe.set_user("Administrator")

# Test your endpoint
result = frappe.call("helpdesk.api.ticket.assign_ticket_to_agent", 
                     ticket_id="TICKET-001", 
                     agent_id="[email protected]")
print(result)

Best Practices

  1. Type Annotations: Always use type hints (required in Helpdesk)
  2. Validation: Validate input parameters before processing
  3. Permissions: Always check permissions before data access
  4. Error Messages: Use translatable error messages with _()
  5. Logging: Log errors using frappe.log_error() for debugging
  6. Transactions: Use frappe.db.commit() for bulk operations
  7. Query Builder: Prefer Query Builder over raw SQL
  8. Documentation: Add docstrings to all API functions
  9. Real-time: Publish events for UI updates when data changes
  10. Caching: Cache expensive read operations with @redis_cache()

Calling APIs from Frontend

See Frontend Components for examples of calling these APIs from Vue components.

Build docs developers (and LLMs) love