Text Classification API
The classify endpoint sorts a piece of text into labels you define in the request — no training data, no per-customer model. Send the text together with 2–20 label names (optionally with a short description of each), and the endpoint returns the best-matching label, a score for every label, and a one-sentence rationale pointing at the evidence in the text.
Two modes are supported:
- Single-label (the default): exactly one label applies. The scores form a probability distribution
over your labels (they sum to ~1) and
labelis the top one. - Multi-label (
multi_label: true): zero, one or several labels may apply at once. Each label gets an independent 0–1 score, andlabelslists every label at or above athresholdyou control.
It is built for support-ticket intent routing, topic tagging, lead qualification, content categorization and feedback bucketing. For fixed category sets, see the sentiment and tone endpoints.
Sample Code
- cURL
- JavaScript
- Python
- Python (Sapling client)
curl -X POST https://api.sapling.ai/api/v1/classify \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?", "labels": ["billing", "technical issue", "shipping", "other"], "context": "Support tickets for a SaaS billing product"}'
import axios from 'axios';
async function run(text) {
try {
const response = await axios.post(
'https://api.sapling.ai/api/v1/classify',
{
key: '<api-key>',
text,
labels: ['billing', 'technical issue', 'shipping', 'other'],
context: 'Support tickets for a SaaS billing product',
},
);
const {status, data} = response;
console.log({status});
console.log(JSON.stringify(data, null, 4));
} catch (err) {
const msg = err.response?.data?.msg || err.message;
console.log({err: msg});
}
}
run('Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?');
import requests
from pprint import pprint
response = requests.post(
"https://api.sapling.ai/api/v1/classify",
json={
"key": "<api-key>",
"text": "Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?",
"labels": ["billing", "technical issue", "shipping", "other"],
"context": "Support tickets for a SaaS billing product",
}
)
if 200 <= response.status_code < 300:
pprint(response.json())
else:
print('Error: ', response.status_code, response.text)
from sapling import SaplingClient
api_key = '<api-key>'
client = SaplingClient(api_key=api_key)
text = 'Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?'
labels = [
{'name': 'billing', 'description': 'Charges, invoices, refunds and payment methods'},
{'name': 'technical issue', 'description': 'Bugs, errors, crashes and outages'},
{'name': 'shipping', 'description': 'Delivery status, addresses and tracking'},
{'name': 'other', 'description': 'Anything that does not fit the other labels'},
]
result = client.classify(text, labels, context='Support tickets for a SaaS billing product')
print(result['label'], '|', result['rationale'])
for entry in result['scores']:
print(f"{entry['label']}: {entry['score']}")
# Multi-label tagging: every label scoring >= threshold is returned in `labels`
tags = client.classify(
'I was charged twice and the app crashes on launch',
labels,
multi_label=True,
threshold=0.5,
)
print(tags['labels']) # ['billing', 'technical issue']
Sample Response
{
"label": "billing",
"labels": ["billing"],
"scores": [
{"label": "billing", "score": 0.86},
{"label": "technical issue", "score": 0.07},
{"label": "shipping", "score": 0.04},
{"label": "other", "score": 0.03}
],
"rationale": "The customer reports being charged twice on one invoice.",
"multi_label": false
}
With "multi_label": true and the text "I was charged twice and the app crashes on launch", several labels
can apply at once and the scores no longer need to sum to 1:
{
"label": "billing",
"labels": ["billing", "technical issue"],
"scores": [
{"label": "billing", "score": 0.92},
{"label": "technical issue", "score": 0.81},
{"label": "shipping", "score": 0.03},
{"label": "other", "score": 0.02}
],
"rationale": "Mentions a duplicate charge and an app crash.",
"multi_label": true
}
Batch Requests
To classify many short texts — a queue of support tickets, a page of reviews — in one request, send
texts (a list of 1–10 strings) instead of text. The same labels, multi_label, threshold and
context apply to every item, and the combined length of all items may be up to 10,000 characters
(the same cap as a single text). Exactly one of text and texts must be provided.
curl -X POST https://api.sapling.ai/api/v1/classify \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "texts": ["I was charged twice this month.", "Where is my package?"], "labels": ["billing", "shipping", "other"]}'
The batch response is {"results": [...]} with one entry per input, in input order; each entry has
exactly the single-response shape above:
{
"results": [
{
"label": "billing",
"labels": ["billing"],
"scores": [
{"label": "billing", "score": 0.91},
{"label": "shipping", "score": 0.05},
{"label": "other", "score": 0.04}
],
"rationale": "Reports a duplicate charge.",
"multi_label": false
},
{
"label": "shipping",
"labels": ["shipping"],
"scores": [
{"label": "shipping", "score": 0.93},
{"label": "other", "score": 0.04},
{"label": "billing", "score": 0.03}
],
"rationale": "Asks about a package's whereabouts.",
"multi_label": false
}
]
}
Items are billed individually (a batch of N costs the same as N single requests), and each item is
cached the same way as a single request — batches and single calls share the cache. If any item fails
to classify, the whole request returns a 502 and nothing is billed; the items that did succeed are
already cached, so a retry only re-runs the failures.
Request Parameters
POST to https://api.sapling.ai/api/v1/classify
key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.
text: String
The text to classify, up to 10,000 characters (measured on the raw input, before any HTML is stripped).
Plain text or HTML — tags are stripped server-side. Text that is empty after stripping returns a 400.
The text is treated as untrusted data: instructions inside it are ignored and it is classified on its content alone.
Provide exactly one of text and texts.
texts: List[String]
Batch form (see Batch Requests): 1–10 texts to classify in one request, each treated
exactly like text above, with a combined length of up to 10,000 characters. An item that is empty after
stripping returns a 400 naming its index. The response becomes {"results": [...]}, one entry per item
in input order.
labels: List[String | Object]
The set of labels to choose from: between 2 and 20 entries. Each entry is either a string (the label name) or an
object {"name": "...", "description": "..."} where description is optional. Names are trimmed, internal
whitespace is collapsed, and they must be at most 50 characters and case-insensitively unique; descriptions
are at most 200 characters. Names are returned in this normalized form in the response, so match your application logic on the normalized names.
A short description per label sharpens borderline decisions — see Tips.
multi_label: Boolean, optional, defaults to false
false: exactly one label applies — scores are a probability distribution over the labels and labels is [label].
true: zero, one or several labels may apply — each label gets an independent 0–1 score and labels contains
every label whose score is at least threshold. Non-boolean values return a 400.
threshold: Number, optional, defaults to 0.5
Multi-label cut-off between 0 and 1: a label is included in labels when score >= threshold.
Ignored in single-label mode. Values outside 0–1 (or non-numbers) return a 400.
context: String, optional
Up to 500 characters describing what the texts are and how to decide between labels, for example
"Support tickets for a SaaS billing product; pick billing for anything about charges or refunds".
Guidance only — it steers the decision but can never add a label outside labels.
Response Parameters
label: String
The best-matching label. Always one of the submitted label names (as normalized: trimmed, internal whitespace collapsed).
labels: List[String]
Single-label mode: [label]. Multi-label mode: every label whose score is at least threshold, in descending score
order — this may be empty, which means none of the labels apply.
scores: List[Object]
Exactly one entry per submitted label, sorted by descending score (ties keep the model's pick first, then input order). Each has:
label: the label name.score: 0–1, rounded to 4 decimals. In single-label mode the scores sum to ~1 (a probability distribution over the labels); in multi-label mode each is an independent applicability probability and they need not sum to 1.
rationale: String
One sentence (at most 300 characters) naming the evidence in the text that drove the decision. May be an empty string.
multi_label: Boolean
Echo of the mode used for this request.
Errors
| Status | Meaning |
|---|---|
400 | Validation error — missing or oversized text, text empty after stripping, both or neither of text/texts provided, an invalid or over-length texts batch, or invalid labels (e.g. {"msg": "Invalid labels: Needs between 2 and 20 labels."}), multi_label, threshold or context. The body is {"msg": "..."}. |
401 / 403 | Missing or invalid API key. |
429 | Rate limit exceeded or key over capacity. |
502 | {"msg": "Unexpected error classifying text."} — the classification model failed; safe to retry. Not billed and not cached. |
Tips
- Describe your labels. Plain names work, but a one-line
descriptionper label (what belongs in it, and what does not) noticeably improves borderline cases, especially when labels overlap (billingvsaccount) or are domain-specific jargon. - Add an "other" label. In single-label mode the endpoint always picks one of your labels, so include a
catch-all (
other,unrelated,none of the above) unless every text is guaranteed to fit one of your categories. - Use
contextto say what the texts are and how to decide — e.g."Inbound sales emails; pick qualified only when a budget or timeline is mentioned". This is usually the cheapest way to fix systematic mistakes. - Multi-label for tagging, single-label for routing. Use
multi_label: truewhen a text can legitimately belong to several buckets (topic tags, feature areas) andfalsewhen you need exactly one decision (queue assignment, intent). - Tune
thresholdfrom the scores. Start at the default0.5; lower it to recall more tags, raise it for precision. Results are cached on Sapling's side for about three days keyed on the text, labels, mode and context — the threshold is applied on top, so re-running with a different threshold returns the same underlying scores. - Scores are calibrated guidance, not guarantees — for high-stakes automation, act on high-confidence results and route low-margin ones (top two scores close together) to a human.