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 draftFreshdesk 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 sentimentCreate 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 evaluationFreshdesk 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 name | Type | Purpose |
|---|---|---|
| AI Category | Dropdown | Stores the Triage classification category |
| AI Urgency | Dropdown | Stores the urgency level from Triage |
| AI Confidence | Single-line text | Stores the classification confidence score |
2. Create custom contact fields
In Admin > Customer Fields, add:
| Field name | Type | Purpose |
|---|---|---|
| Sentiment Score | Dropdown | positive, neutral, negative |
| Escalation Risk | Single-line text | Numeric 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:
| Plan | Rate limit |
|---|---|
| Free | 50 requests/minute |
| Growth | 200 requests/minute |
| Pro | 400 requests/minute |
| Enterprise | 750 requests/minute |
Keep these in mind:
- Handle 429 responses. When Freshdesk returns
429 Too Many Requests, read theRetry-Afterheader:
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
- Integration Overview -- Architecture and other platform guides
- Generic Webhook -- Build a platform-agnostic integration