chat.completions endpoint with tools. When the model returns a tool_call, your code runs the matching Freshdesk or order-DB query and feeds the result back. The model writes the customer reply. Add three guardrails — confirm-before-write, a confidence floor, and a human-handoff function — and you have a safe v1.
## Why Function Calling, Not a Decision Tree
Old-style chatbots used keyword rules: if the message contains "refund," show the refund flow. They break the moment a customer types "I got the wrong cushion cover, send my money back." Function calling flips this. You describe what your system can do as a list of functions, and the model decides which one fits the customer's actual sentence — including messy, multi-intent ones. OpenAI shipped this as stable tools/tool_calls in the Chat Completions API, and as of July 2025 every current model supports parallel tool calls (the model can ask to run two functions in one turn).
The practical win: you stop writing intent classifiers. You write database queries and let the model route to them.
- An OpenAI API key with billing enabled — budget ₹500 for the first week
- A Freshdesk account (the Growth plan or higher exposes the Tickets API)
- Your Freshdesk domain and an API key from Profile Settings
- Node.js 20+ (the code below is JavaScript; Python is near-identical)
- Read access to wherever order data lives — Shopify Admin API, a Postgres table, or a Google Sheet for a quick start
- One test order you can safely look up and one you can safely refund
// tools.js
export const tools = [
{
type: "function",
function: {
name: "get_order_status",
description: "Look up the current shipment status of an order. Use when the customer asks where their order is, or for tracking. Requires an order ID OR the email used at checkout.",
parameters: {
type: "object",
properties: {
order_id: { type: "string", description: "Order ID, e.g. JD-10482" },
email: { type: "string", description: "Email used at checkout" }
},
required: []
}
}
},
{
type: "function",
function: {
name: "create_refund_ticket",
description: "Create a refund request ticket. ONLY call after the customer has explicitly confirmed they want a refund and you have the order ID.",
parameters: {
type: "object",
properties: {
order_id: { type: "string" },
reason: { type: "string", description: "Why the customer wants a refund" },
email: { type: "string" }
},
required: ["order_id", "reason", "email"]
}
}
},
{
type: "function",
function: {
name: "check_pincode_serviceable",
description: "Check whether a 6-digit Indian pincode is serviceable for delivery.",
parameters: {
type: "object",
properties: { pincode: { type: "string", description: "6-digit pincode" } },
required: ["pincode"]
}
}
},
{
type: "function",
function: {
name: "escalate_to_human",
description: "Hand off to a human agent. Call this when you are unsure, the customer is upset, the request is a complaint, or the topic is outside orders/refunds/shipping.",
parameters: {
type: "object",
properties: { summary: { type: "string", description: "One-line summary for the agent" } },
required: ["summary"]
}
}
}
];
description field is prompt engineering in disguise. We rewrote create_refund_ticket's description three times before the model stopped creating refund tickets on the first "I might want a refund." Specific verbs ("ONLY call after the customer has explicitly confirmed") fixed it.// chat.js
import OpenAI from "openai";
import { tools } from "./tools.js";
import { runTool } from "./handlers.js";
const openai = new OpenAI();
const SYSTEM = You are the support assistant for Jaipur Decor.
Be warm, concise, and reply in the customer's language (Hindi or English).
You can look up orders, check pincodes, and start refunds.
Never invent an order status — always call get_order_status.
If you are unsure, call escalate_to_human.;
export async function reply(history) {
const messages = [{ role: "system", content: SYSTEM }, ...history];
for (let hop = 0; hop < 5; hop++) {
const res = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
tool_choice: "auto",
temperature: 0.3
});
const msg = res.choices[0].message;
messages.push(msg);
if (!msg.tool_calls) {
return msg.content; // plain reply for the customer
}
for (const call of msg.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = await runTool(call.function.name, args);
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result)
});
}
}
return "Let me get a teammate to help with this.";
}
The for loop cap (5 hops) is a guardrail in itself — it stops a runaway model from calling tools forever. We have never seen a legitimate support turn need more than three hops.
## Step 3: Wire the Handlers to Freshdesk and Your Order DB
The handlers are plain functions. Here are the two that touch real systems — the order lookup (read) and the Freshdesk ticket (write).
// handlers.js
const FD = process.env.FRESHDESK_DOMAIN; // e.g. jaipurdecor
const FD_KEY = process.env.FRESHDESK_API_KEY;
export async function runTool(name, args) {
if (name === "get_order_status") {
// Replace with your real source — Shopify, Postgres, etc.
const order = await db.orders.findByIdOrEmail(args.order_id, args.email);
if (!order) return { found: false };
return { found: true, status: order.status, courier: order.courier,
tracking: order.tracking_url, eta: order.eta };
}
if (name === "create_refund_ticket") {
const auth = Buffer.from(${FD_KEY}:X).toString("base64");
const r = await fetch(https://${FD}.freshdesk.com/api/v2/tickets, {
method: "POST",
headers: { "Content-Type": "application/json",
"Authorization": Basic ${auth} },
body: JSON.stringify({
subject: Refund request — order ${args.order_id},
description: Reason: ${args.reason},
email: args.email,
priority: 2, status: 2,
tags: ["refund", "bot-created"]
})
});
const ticket = await r.json();
return { ticket_id: ticket.id, created: true };
}
if (name === "check_pincode_serviceable") {
const ok = await courier.isServiceable(args.pincode);
return { pincode: args.pincode, serviceable: ok };
}
if (name === "escalate_to_human") {
await freshdeskAssignToGroup(args.summary);
return { escalated: true };
}
}
Freshdesk's Tickets API uses HTTP Basic auth with your API key as the username and any string as the password — that ${FD_KEY}:X is correct, not a typo. The priority: 2, status: 2 maps to Medium/Open in Freshdesk's enums.
get_order_status call and a reply quoting the real status and tracking URL. If the model invents a status, your system prompt's "never invent" line needs strengthening.create_refund_ticket only after you say yes. Check Freshdesk for a real ticket tagged "refund" and "bot-created".gpt-4o-mini if your queries need deep reasoning over policy documents — that's a RAG problem, covered in our RAG helpdesk bot guide, not a function-calling one.gpt-4o-mini rates and ~120 chats/day, the Jaipur brand spent about ₹1,500–₹2,000/month in API tokens. Each chat ran 2–4 model calls. Heavier reasoning models cost 10–20x more, so start cheap and upgrade only the conversations that need it.
### Which OpenAI model should I use for a support bot?
Start with gpt-4o-mini — it handles function routing and friendly replies well at a low price. Move to a larger model only if you see the bot picking wrong functions or writing weak replies. Test the cheap model first; most support flows never need more.
### Can the bot reply in Hindi?
Yes. Add one line to the system prompt — "reply in the customer's language" — and the model matches Hindi, Hinglish, or English automatically. For voice or heavy regional-language support, pair it with a dedicated ASR; see our Hindi-first chatbot tutorial.
### How do I stop the bot from issuing refunds it shouldn't?
Two layers. Put "confirm before calling create_refund_ticket" in both the system prompt and the function's description, and keep refunds as ticket creation (a human approves the payout) rather than direct money movement in v1. Never let a v1 bot move money unattended.
### Does this work with Zendesk or Intercom instead of Freshdesk?
Yes. Only the handler functions change — swap the Freshdesk REST call for the Zendesk or Intercom ticket API. The function schemas and the OpenAI loop stay identical. That portability is the whole point of the function-calling design.
### How do I measure if the bot is actually helping?
Track three numbers weekly: resolution rate (chats closed without a human), escalation rate (clean handoffs), and mishandle rate (wrong function or bad reply, found by sampling 20 transcripts). A healthy v1 sits around 55–65% resolution with under 15% mishandles.
Want a support chatbot live on your helpdesk this month?
We ship a working OpenAI or Claude function-calling bot wired into Freshdesk, Zendesk, or WhatsApp for Indian SMBs in 5–7 working days. Typical project: ₹45k–₹85k. Suitable if you're drowning in repetitive support chats and have order data behind an API. No slides — just your problem and our honest take.
Book a 20-min Call
