AI Support Guide

Freshdesk

Connect Simpli services to Freshdesk for AI-powered ticket handling.

Freshdesk by Freshworks is a popular helpdesk platform with strong automation capabilities. This guide walks through connecting every Simpli service to Freshdesk using automation rules, webhooks, and the Freshdesk API v2.

Freshdesk remains your system of record. Simpli adds AI capabilities alongside it — auto-classifying tickets, drafting responses, scoring conversations, and tracking customer health.

Prerequisites

  • Freshdesk account (any plan with API access — Growth or higher recommended for automation rules)
  • Freshdesk API key — find it at Profile icon > Profile Settings > Your API Key
  • AI capabilities deployed and running
  • A middleware endpoint — your own server or serverless function that sits between Freshdesk and Simpli

Architecture

┌──────────┐   automation/webhook   ┌──────────────┐     REST API      ┌─────────────────┐
│ Freshdesk│ ────────────────────► │  Middleware   │ ────────────────► │ Simpli Services  │
│          │ ◄──────────────────── │  (your code)  │ ◄──────────────── │ Triage, Reply... │
└──────────┘   Freshdesk API v2    └──────────────┘    AI results      └─────────────────┘

Freshdesk fires webhooks via automation rules on ticket events. Your middleware receives these, calls the relevant Simpli APIs, and pushes results back to Freshdesk via its REST API. No marketplace apps required.

Authentication

Freshdesk API v2 uses Basic Auth with the API key as the username and X as the password. The key is sent base64-encoded in the Authorization header.

import httpx
import base64

FRESHDESK_DOMAIN = "yourcompany"  # yourcompany.freshdesk.com
FRESHDESK_API_KEY = "your-api-key"

# httpx handles Basic Auth natively
FD_AUTH = (FRESHDESK_API_KEY, "X")
FD_BASE = f"https://{FRESHDESK_DOMAIN}.freshdesk.com/api/v2"

All API examples below use these constants.

Per-service integration

Triage — auto-classify and route tickets

When: A new ticket is created in Freshdesk.

Flow: An automation rule fires a webhook to your middleware on ticket creation. Middleware calls Triage /classify and /route, then updates the Freshdesk ticket with the results — setting the type, priority, group, and custom fields.

TRIAGE_URL = "http://localhost:8001"

async def classify_and_route(ticket_id: int, subject: str, description: str):
    async with httpx.AsyncClient() as client:
        # Classify the ticket
        classify_resp = await client.post(f"{TRIAGE_URL}/classify", json={
            "subject": subject,
            "body": description,
        })
        classification = classify_resp.json()

        # Route based on classification
        route_resp = await client.post(f"{TRIAGE_URL}/route", json={
            "ticket_id": str(ticket_id),
            "category": classification["category"],
            "urgency": classification["urgency"],
            "sentiment": classification["sentiment"],
        })
        routing = route_resp.json()

        # Map urgency to Freshdesk priority (1=Low, 2=Medium, 3=High, 4=Urgent)
        priority_map = {"low": 1, "medium": 2, "high": 3, "critical": 4}

        # Update the Freshdesk ticket
        await client.put(
            f"{FD_BASE}/tickets/{ticket_id}",
            auth=FD_AUTH,
            json={
                "priority": priority_map.get(classification["urgency"], 2),
                "type": classification["category"].replace("_", " ").title(),
                "group_id": routing.get("group_id"),
                "custom_fields": {
                    "cf_ai_category": classification["category"],
                    "cf_ai_urgency": classification["urgency"],
                    "cf_ai_confidence": str(classification["confidence"]),
                },
                "tags": [
                    f"ai-category:{classification['category']}",
                    f"ai-urgency:{classification['urgency']}",
                ],
            },
        )

    return {"classification": classification, "routing": routing}

Custom field names in the Freshdesk API are prefixed with cf_ and use snake_case. Create the fields first in Admin > Ticket Fields before referencing them.

Reply — AI draft responses

When: A new ticket is classified (chain this after Triage).

Flow: After classification, middleware calls Reply /api/v1/draft and posts the draft as a private note on the ticket. Agents see the suggestion and can copy or edit it before replying to the customer.

REPLY_URL = "http://localhost:8002"

async def draft_reply(ticket_id: int, conversation: list[dict]):
    async with httpx.AsyncClient() as client:
        # Generate a draft
        draft_resp = await client.post(f"{REPLY_URL}/api/v1/draft", json={
            "ticket_id": str(ticket_id),
            "conversation": conversation,
            "style": "friendly",
            "language": "en",
        })
        draft = draft_resp.json()

        # Post as private note in Freshdesk
        await client.post(
            f"{FD_BASE}/tickets/{ticket_id}/notes",
            auth=FD_AUTH,
            json={
                "body": (
                    f"<strong>AI Draft Response</strong> "
                    f"(confidence: {draft['confidence']:.0%})<br><br>"
                    f"{draft['draft']}<br><br>"
                    f"<em>Copy and edit this draft before sending.</em>"
                ),
                "private": True,
            },
        )

    return draft

Freshdesk notes use HTML formatting rather than Markdown. The private: true flag ensures only agents see the draft.

Sentiment — customer health tracking

When: A customer replies to a ticket.

Flow: An automation rule fires on customer reply. Middleware calls Sentiment /analyze, then updates a custom field on the contact record with the health score.

SENTIMENT_URL = "http://localhost:8004"

async def analyze_reply(ticket_id: int, contact_id: int, reply_text: str):
    async with httpx.AsyncClient() as client:
        # Analyze sentiment
        sentiment_resp = await client.post(f"{SENTIMENT_URL}/analyze", json={
            "customer_id": str(contact_id),
            "text": reply_text,
            "channel": "web",
        })
        sentiment = sentiment_resp.json()

        # Update contact custom field with sentiment score
        await client.put(
            f"{FD_BASE}/contacts/{contact_id}",
            auth=FD_AUTH,
            json={
                "custom_fields": {
                    "cf_sentiment_score": sentiment["label"],
                    "cf_escalation_risk": str(sentiment["escalation_risk"]),
                },
            },
        )

        # Tag the ticket if escalation risk is high
        if sentiment["escalation_risk"] >= 0.5:
            ticket_resp = await client.get(
                f"{FD_BASE}/tickets/{ticket_id}",
                auth=FD_AUTH,
            )
            existing_tags = ticket_resp.json().get("tags", [])
            existing_tags.append("escalation-risk:high")

            await client.put(
                f"{FD_BASE}/tickets/{ticket_id}",
                auth=FD_AUTH,
                json={"tags": existing_tags},
            )

    return sentiment

Create custom contact fields in Admin > Customer Fields. Agents can filter contacts by cf_sentiment_score to find unhappy customers.

QA — conversation scoring

When: A ticket status changes to Resolved or Closed.

Flow: Middleware pulls the full conversation from Freshdesk, sends it to QA /evaluate, and posts the score as a private note.

QA_URL = "http://localhost:8003"

async def score_conversation(ticket_id: int):
    async with httpx.AsyncClient() as client:
        # Pull conversation from Freshdesk
        conv_resp = await client.get(
            f"{FD_BASE}/tickets/{ticket_id}/conversations",
            auth=FD_AUTH,
        )
        conversations = conv_resp.json()

        # Convert to Simpli message format
        messages = []
        for entry in conversations:
            role = "customer" if entry["incoming"] else "agent"
            messages.append({"role": role, "content": entry["body_text"]})

        # Evaluate the conversation
        qa_resp = await client.post(f"{QA_URL}/evaluate", json={
            "conversation_id": str(ticket_id),
            "agent_id": str(conversations[-1].get("user_id", "")),
            "messages": messages,
        })
        evaluation = qa_resp.json()

        # Post score as private note
        dimensions = evaluation.get("dimensions", {})
        dimension_lines = "".join(
            f"<li><strong>{k.title()}</strong>: {v:.0%}</li>"
            for k, v in dimensions.items()
        )
        await client.post(
            f"{FD_BASE}/tickets/{ticket_id}/notes",
            auth=FD_AUTH,
            json={
                "body": (
                    f"<strong>QA Score: {evaluation['overall_score']:.0%}</strong>"
                    f"<ul>{dimension_lines}</ul>"
                    f"<p><strong>Coaching notes:</strong> "
                    f"{', '.join(evaluation.get('coaching_notes', []))}</p>"
                ),
                "private": True,
            },
        )

    return evaluation

Freshdesk conversations use the incoming boolean to distinguish customer messages from agent replies. The body_text field gives you the plain-text version without HTML.

KB — article sync

Sync your Freshdesk Solutions knowledge base articles into Simpli KB, then use gap analysis to find missing content.

KB_URL = "http://localhost:8006"

async def sync_freshdesk_articles():
    async with httpx.AsyncClient() as client:
        # Fetch all solution categories
        categories_resp = await client.get(
            f"{FD_BASE}/solutions/categories",
            auth=FD_AUTH,
        )

        for category in categories_resp.json():
            # Fetch folders in each category
            folders_resp = await client.get(
                f"{FD_BASE}/solutions/categories/{category['id']}/folders",
                auth=FD_AUTH,
            )

            for folder in folders_resp.json():
                # Fetch articles in each folder
                articles_resp = await client.get(
                    f"{FD_BASE}/solutions/folders/{folder['id']}/articles",
                    auth=FD_AUTH,
                )

                for article in articles_resp.json():
                    await client.post(f"{KB_URL}/articles", json={
                        "title": article["title"],
                        "content": article["description_text"],
                        "tags": article.get("tags", []),
                        "source": "freshdesk-solutions",
                    })

        # Check for content gaps
        gaps_resp = await client.get(f"{KB_URL}/gaps")
        return gaps_resp.json()

Freshdesk Solutions are organized as Categories > Folders > Articles. The sync iterates through all three levels to capture every article.

Freshdesk configuration steps

1. Create custom ticket fields

In Admin > Ticket Fields, add:

Field nameTypePurpose
AI CategoryDropdownStores the Triage classification category
AI UrgencyDropdownStores the urgency level from Triage
AI ConfidenceSingle-line textStores the classification confidence score

2. Create custom contact fields

In Admin > Customer Fields, add:

Field nameTypePurpose
Sentiment ScoreDropdownpositive, neutral, negative
Escalation RiskSingle-line textNumeric risk score from Sentiment

3. Set up automation rules

In Admin > Automations > Ticket Creation:

Rule: AI Classify New Ticket

  • Conditions: When a ticket is created (you can add filters like specific groups or channels)
  • Action: Trigger webhook to https://your-server.example.com/freshdesk/ticket-created
  • Callback URL: Your middleware endpoint
  • Content: JSON with {{ticket.id}}, {{ticket.subject}}, {{ticket.description}}

In Admin > Automations > Ticket Updates:

Rule: AI Score Resolved Ticket

  • Conditions: When status changes to Resolved
  • Action: Trigger webhook to https://your-server.example.com/freshdesk/ticket-resolved

4. Automation rules vs observer rules

Freshdesk has two automation types:

  • Automation rules (Ticket Creation / Ticket Updates): Run once when conditions are met. Best for Triage on creation and QA on resolution.
  • Observer rules: Run when an agent performs an action. Best for Reply drafts after an agent opens a ticket.

Use Ticket Creation automations for Triage and Sentiment on new tickets. Use Ticket Update automations for QA when tickets are resolved.

Rate limits and best practices

Freshdesk API rate limits vary by plan:

PlanRate limit
Free50 requests/minute
Growth200 requests/minute
Pro400 requests/minute
Enterprise750 requests/minute

Keep these in mind:

  • Handle 429 responses. When Freshdesk returns 429 Too Many Requests, read the Retry-After header:
resp = await client.put(url, auth=FD_AUTH, json=data)
if resp.status_code == 429:
    retry_after = int(resp.headers.get("Retry-After", 60))
    await asyncio.sleep(retry_after)
    resp = await client.put(url, auth=FD_AUTH, json=data)
  • Keep middleware idempotent. Freshdesk may retry webhooks on failure. Use the ticket ID to prevent duplicate processing.
  • Use HTML in notes. Freshdesk note bodies support HTML, not Markdown. Use <strong>, <em>, <ul> tags.
  • Log everything. Store the Simpli API responses alongside the Freshdesk ticket ID for debugging.

Next steps

On this page