How to Build a Claude-Powered Email Classifier
What you're actually building
An email classifier takes an incoming message — subject, body, sender, maybe headers — and assigns it to a category: support request, sales lead, invoice, spam, urgent escalation, newsletter, and so on. You then route it: create a ticket, notify a human, auto-reply, or archive it. Claude is a good fit for this because email text is messy and rule-based filters (regex, keyword lists) break constantly. Claude reads intent, not just keywords, so "I haven't received my refund and this is the third time I'm emailing" gets classified as urgent/billing even without the word "urgent" in it.
The core pattern is simple: send the email content to Claude with a fixed set of categories and a strict output format, parse the response, and act on it. The rest of this guide covers how to make that reliable at production volume: schema enforcement, confidence scoring, batching, and cost control.
Step 1: Define your taxonomy before writing any code
Classification quality depends more on your category definitions than on prompt cleverness. Vague categories ("general," "other") produce vague results. Write definitions a human support agent could follow without asking questions:
- billing: payment failures, refund requests, invoice questions, subscription changes
- bug_report: describes unexpected product behavior, errors, crashes
- feature_request: asks for new functionality or improvements
- sales_lead: pricing questions, demo requests, from a non-customer domain
- urgent_escalation: repeated contact, threatens cancellation, legal/compliance language
- spam: unsolicited marketing, phishing, irrelevant content
Six to ten categories is a reasonable range. More than that, and you should consider a two-stage classifier (broad category, then sub-category) rather than one giant prompt.
Step 2: Force structured output with tool use
Don't ask Claude to classify and then regex the answer out of a sentence. Use tool calling so the response is always valid JSON you can parse without guessing. This is the single biggest reliability improvement over free-text prompting — see the tool use docs for the full spec if you're integrating through SubToAPI.
const tools = [
{
name: "classify_email",
description: "Classify an email into exactly one category",
input_schema: {
type: "object",
properties: {
category: {
type: "string",
enum: [
"billing",
"bug_report",
"feature_request",
"sales_lead",
"urgent_escalation",
"spam"
]
},
confidence: { type: "number", minimum: 0, maximum: 1 },
reason: { type: "string" }
},
required: ["category", "confidence", "reason"]
}
}
];
The enum constraint is doing most of the work here — Claude physically cannot return a category outside your list. The confidence field lets you route low-confidence results to a human queue instead of silently misfiling them. reason is cheap to include and invaluable when you're debugging why something landed in the wrong bucket.
Step 3: Write a tight prompt
Keep the system prompt focused on the task, not the formatting (tool use already handles formatting):
You classify customer emails for a software company.
Read the subject and body carefully. Pick exactly one category
from the provided list. If the email could fit two categories,
choose the one that reflects the sender's primary intent, not
secondary mentions. Set confidence below 0.6 if the email is
ambiguous, very short, or mixes multiple intents.
Then send the actual email as the user message, with sender and subject clearly labeled so Claude doesn't have to guess which part is which:
From: priya@acmecorp.io
Subject: Still waiting on refund — 3rd email
Hi, I emailed twice last week about my refund for the
cancelled plan and haven't heard back. This is getting
frustrating, I need this resolved today.
Step 4: Call the API and route on the result
Here's a minimal end-to-end call through SubToAPI, which wraps your existing Claude access in a standard HTTPS endpoint:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 200,
"tools": [...],
"tool_choice": {"type": "tool", "name": "classify_email"},
"messages": [
{"role": "user", "content": "From: priya@acmecorp.io\nSubject: Still waiting on refund\n\nHi, I emailed twice..."}
]
}'
Forcing tool_choice to the classifier tool guarantees Claude always calls it rather than responding with plain text — no fallback parsing needed. Full request/response shape is in the messages docs.
In your application code, route on category and confidence:
const result = await classifyEmail(email);
if (result.confidence < 0.6) {
await sendToReviewQueue(email, result);
} else if (result.category === "urgent_escalation") {
await notifySlack(email, result);
} else {
await createTicket(email, result.category);
}
Step 5: Handle volume without burning through limits
Email classification is a high-volume, low-token task — most emails are a few hundred tokens, and the response is a tiny JSON object. The main operational risk isn't cost per call, it's hitting rate limits during traffic spikes (Monday morning inbox dumps, post-outage support floods). A few practical steps:
- Batch where you can. If latency isn't critical (overnight queue processing), group emails and process them with a small concurrency limit instead of firing everything at once.
- Cache repeat senders. If the same sender emails about the same ticket thread, you often don't need to reclassify every reply — classify the thread once and only reclassify if the subject or intent clearly changes.
- Track usage per team or per mailbox. If you're running this across multiple client mailboxes or internal teams, you need visibility into who's consuming what. SubToAPI's dashboard gives you per-key usage and seat management out of the box, which matters once more than one person or system is calling the classifier — see pricing for plan details.
- Set
max_tokenslow. Classification responses are short; capping output tokens prevents runaway generations and keeps latency predictable.
Step 6: Monitor drift
Categories that made sense at launch stop being accurate as your product changes. Log every classification with its confidence score and spot-check the low-confidence bucket weekly. If a new category keeps almost-fitting into "other," that's a signal to add it to the taxonomy rather than tuning the prompt further.
If you're starting from scratch, the fastest path is the quickstart guide — get a sub_live_ key, send a test email through the tool-use example above, and build the routing logic once the output format is predictable. A free trial at signup is enough to validate the taxonomy before wiring it into production.
Questions
Can Claude classify emails into more than one category at once? Yes, but it's cleaner to run a primary classification first, then a secondary pass only if needed. Multi-label output in a single call tends to produce inconsistent confidence scoring compared to single-label, enum-constrained tool calls.
How do I handle emails in languages other than English? Claude handles multilingual input natively — you don't need separate prompts per language. Just make sure your category definitions and few-shot examples don't assume English-only phrasing if your inbox is multilingual.
Should I use streaming for a classifier? No. Classification output is small and you need the complete, valid JSON object before acting on it, so a standard non-streaming request is simpler and sufficient here — streaming is more useful for longer, user-facing generations.