Claude API Customer Ticket Triage Automation
Support teams waste hours every day manually reading tickets, guessing at priority, and routing them to the right queue. Claude API customer ticket triage automation solves this by classifying incoming tickets — category, urgency, sentiment, and suggested team — in a single API call, before a human ever opens them. This article shows the actual architecture and code for building that pipeline.
The core idea is simple: every time a ticket arrives (from your helpdesk, email inbox, or chat widget), you send its text to Claude with a structured prompt asking for a JSON classification. The model returns category, priority, sentiment, and a short summary, which your backend then uses to route the ticket, set its priority flag, or auto-assign it to a team. No manual tagging, no rules engine with hundreds of if/else branches.
Why triage is a good fit for LLM automation
Traditional ticket routing relies on keyword rules ("if subject contains 'refund', route to billing") that break constantly — customers don't write in keywords, they write in sentences. Claude reads the actual meaning of a ticket: a message like "I was charged twice and nobody is answering me" should be flagged as billing + urgent + frustrated, even without the word "refund" or "angry" appearing anywhere.
This matters for three reasons:
- Consistency — every ticket gets evaluated against the same criteria, no reviewer fatigue or bias.
- Speed — classification happens in under a second, before the ticket even lands in an agent's queue.
- Escalation accuracy — sentiment and urgency detection catch angry or high-churn-risk customers that a first-come-first-served queue would otherwise bury.
Designing the classification schema
Before writing any code, define what fields you actually need downstream. A typical triage schema looks like:
{
"category": "billing | technical | account | feature_request | other",
"priority": "low | medium | high | urgent",
"sentiment": "positive | neutral | negative | angry",
"summary": "one-sentence summary of the issue",
"suggested_team": "billing | support | engineering"
}
Keep the schema small. The more fields you ask for, the more likely the model drifts into inconsistent formatting. Five to seven fields is usually the sweet spot for ticket triage.
Building the triage call
The prompt should instruct Claude to return strict JSON and nothing else — this makes parsing reliable on your side.
const ticketText = `Subject: Double charge on my account
Body: I was charged twice this month for the Pro plan and support hasn't
responded in 3 days. This is the second time this has happened.`;
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 300,
messages: [{
role: "user",
content: `Classify this support ticket. Return ONLY valid JSON with keys:
category, priority, sentiment, summary, suggested_team.
Ticket:
${ticketText}`
}]
})
});
const data = await response.json();
const classification = JSON.parse(data.content[0].text);
console.log(classification);
Running this against the example ticket should return something close to:
{
"category": "billing",
"priority": "urgent",
"sentiment": "angry",
"summary": "Customer double-charged for Pro plan, second occurrence, no response in 3 days.",
"suggested_team": "billing"
}
That object maps directly to fields in your ticketing system (Zendesk custom fields, a Postgres column, whatever you use) — no further processing needed.
Handling batches and backlogs
If you're triaging a backlog instead of streaming live tickets, send them in small batches rather than one giant prompt. Classification accuracy drops when you cram 50 tickets into a single request and ask for 50 JSON objects back — the model starts losing track of which fields belong to which ticket. Process tickets individually or in groups of 3-5 with clearly numbered boundaries, and parallelize the requests instead.
async function triageTicket(ticket) {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 300,
messages: [{ role: "user", content: buildTriagePrompt(ticket) }]
})
});
return JSON.parse((await res.json()).content[0].text);
}
const results = await Promise.all(tickets.map(triageTicket));
Where SubToAPI fits
Most triage pipelines like this one run as a background job or webhook handler sitting between your helpdesk and your team's routing logic. Instead of wiring up raw Claude credentials and managing them per-service, you issue a dedicated sub_live_... key through SubToAPI for the triage worker — separate from the key your chatbot or internal tools use. That gives you per-service usage metadata, so you can see exactly how many tokens your triage job burns versus everything else on the same Claude access, without extra accounting work. Setup takes a few minutes — see the quickstart and messages endpoint docs for the full request/response shape.
Routing and priority logic after classification
Once you have structured output, the routing layer is just normal backend code:
- Map
suggested_teamto a queue ID in your helpdesk. - Map
priority: urgentorsentiment: angryto an immediate Slack/PagerDuty alert. - Store
summaryas the ticket's internal note so agents don't have to re-read the full message. - Log
categorydistribution over time to spot recurring product issues.
If ticket volume is high and you want classifications to appear as each piece of text is generated (useful for live dashboards), Claude's streaming mode works the same way here — see streaming for implementation details.
Common pitfalls
- Free-text leakage: if the model returns explanatory text before the JSON, your parser breaks. Explicitly instruct "Return ONLY valid JSON" and consider stripping markdown code fences before parsing.
- Inconsistent enums: constrain category/priority values in the prompt itself (list them exactly) rather than letting the model invent new labels.
- No fallback: always wrap
JSON.parsein a try/catch and route unparseable responses to a manual review queue instead of silently dropping tickets.
FAQ
Can Claude triage tickets in languages other than English?
Yes. Claude handles multilingual input well — you can ask it to classify tickets in their original language and still return the JSON schema in English, which keeps your downstream routing logic language-agnostic.
How accurate is LLM-based triage compared to manual tagging?
For common categories (billing, technical, account) accuracy is typically very high since these distinctions rely on clear context rather than ambiguous judgment calls. Edge cases — like tickets mixing multiple issues — still benefit from a human review step, so most teams route only high-confidence, non-urgent classifications automatically at first.
Do I need function/tool calling for this, or is a plain JSON prompt enough?
A plain JSON-output prompt is usually sufficient for ticket triage since the task is single-step classification. Tool calling becomes useful if triage needs to trigger actions directly, like creating a ticket in an external system — see tool use if you want the model to call functions as part of the pipeline.