A Jaipur home-decor brand was paying two agents to answer the same four questions all day: "where's my order," "I want a refund," "do you ship to my pincode," and "is this back in stock." On a Sunday in July 2025 we wired an OpenAI function-calling chatbot into their Freshdesk account. By Monday morning it was answering 61% of incoming chats without a human, and creating clean refund tickets for the rest. Total build time: about seven hours. This is the full walkthrough — code included.
The 60-Word Answer
Define your four most common support tasks as JSON function schemas. Pass them to OpenAI's 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.
What You'll Need (Prerequisites Checklist)
- An OpenAI API key with billing enabled
- 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
Step 1: Describe Your Functions as JSON Schemas
This is 80% of the work. Each function gets a name, a one-line description the model reads to decide when to use it, and a typed parameter list. Be specific in the description — that text is your only steering wheel.
// 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.Step 2: The Function-Calling Loop
The pattern is a loop: send the conversation, check if the model wants to call a tool, run it, send the result back, repeat until the model returns a plain text reply for the customer.
// 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.
Step 4: Verify It Works
Run two test conversations before you connect it to live chat:
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".The 3 Guardrails That Make This Safe to Ship
A read-only bot is safe by default. The moment it can write — create tickets, issue refunds, change addresses — you need these three.
When NOT to Build This
This pattern is wrong for some cases, and pretending otherwise is how you ship a bot that embarrasses the brand.
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.One more honest limit: function calling does not "understand" your business. It maps sentences to functions. If a customer asks something your four functions can't answer, a well-built bot escalates — a badly built one hallucinates. The escalation rate is the metric that tells you which one you shipped.
Real Example: The Jaipur Decor Build
We shipped the four functions above on a Sunday. Order data came from their Shopify Admin API; refunds and escalations went into Freshdesk. The first week's numbers: 61% of chats fully resolved by the bot, 28% escalated cleanly with context attached, 11% the bot mishandled (mostly multi-order messages we hadn't designed for). We added a fifth function for multi-order lookups the next weekend. By week three, one agent moved to handling returns logistics full-time — the queue no longer needed two people on chat.
We applied the same function-calling spine when our AI automation team later built a 3-day Diwali support chatbot on Claude Haiku + Freshdesk + WhatsApp — same skeleton, different model, WhatsApp instead of web chat. The voice version of this idea powers part of TalkDrill, our in-house English-speaking app, where the model routes learners to the right practice scenario.
| Approach | Build time | Handles messy phrasing | Maintenance |
|---|---|---|---|
| Keyword decision tree | 2–3 days | Poorly | High — every new phrasing breaks it |
| OpenAI function calling | 1 Sunday | Well | Low — add a function, not a rule |
| Full RAG over docs | 1–2 weeks | Well | Medium — re-index when docs change |
Frequently Asked Questions
How much does an OpenAI function-calling chatbot cost to run per month?
Atgpt-4o-mini rates and ~120 chats/day, the Jaipur brand's monthly API token spend stayed low. 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 withgpt-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. 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 CallRelated reading
As Hrishikesh, our CTO, puts it: a support bot's job is not to be clever, it's to be reliable about four boring things and honest about everything else.
