Tracking trends and conversation insights
Use the Bloobability assess endpoint to score conversations against your own policies, estimate how often each applies, and track how those rates move over time.
The Bloobability assess endpoint answers one question, per policy: how confident is the model that this policy applies to this conversation? You send a conversation and a list of policies you care about — "customer asked for a refund," "customer is at risk of churning," "agent resolved the issue" — and get back a calibrated probability for each one.
That single primitive is enough to build two things:
- Conversation insights — tag what happened in any one conversation (for routing, review queues, or a live dashboard).
- Trends — measure how often a policy applies across many conversations, and watch that rate move week over week.
Note
This is a v4 feature under the AI tag. All requests go to
https://api.blooio.com/v4. Make sure the version toggle at the top of the docs is set to v4.
How it works
Every call runs the gauge: it scores each policy you send independently and returns a calibrated probability, a yes/no verdict, a calibration tier, and a frozen accept/defer decision. That's the whole product, and it's the fast path.
Optionally, set include_evidence: true to also run the matcher, which attaches evidence_ids — the message IDs supporting each policy — in parallel. Use it when a human needs to see why; leave it off otherwise.
The call is stateless. Nothing is stored, there's no idempotency key, and replaying a request re-runs inference. You bring the conversations; Blooio scores them.
Step 1 — Define your policies
A policy is an id (the stable key you'll track over time) and a description (what the model actually judges against). Write descriptions the way you'd brief a new support lead — plain, specific, one idea each.
[
{ "id": "refund_request", "description": "The customer asks for money back or a refund." },
{ "id": "churn_risk", "description": "The customer signals they may cancel or stop using the product." },
{ "id": "positive_sentiment", "description": "The customer expresses satisfaction or praise." }
]Warning
The
idis the calibration key. Keep it stable and keep it honest: anidthat matches a policy the model was calibrated on returns therostertier (with a validatedaccepteddecision), while a novelidreturnspooled_unseen. If you reuse a knownidfor a different meaning, you'll get a "trusted" label on a number that isn't. Don't rename a policy mid-series and don't recycle anidfor a new concept.
Step 2 — Score a conversation
Send the conversation messages (in order) and your policies. Roles must be customer, agent, or system, and every message id must be unique.
curl -X POST https://api.blooio.com/v4/ai/bloobability/assess \
-H "Authorization: Bearer YOUR_BLOOIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "conv-8891",
"conversation": [
{ "id": "m1", "role": "customer", "content": "This is the third time it broke. I want my money back." },
{ "id": "m2", "role": "agent", "content": "I am sorry — let me pull up your order." }
],
"policies": [
{ "id": "refund_request", "description": "The customer asks for money back or a refund." },
{ "id": "churn_risk", "description": "The customer signals they may cancel or stop using the product." }
],
"include_evidence": true
}'Try itThe response returns one result per policy, in the same order you sent them:
{
"reference_id": "conv-8891",
"policies": [
{
"id": "refund_request",
"calibrated_probability": 0.981,
"verdict": "yes",
"calibration_tier": "roster",
"accepted": true,
"evidence_ids": ["m1"]
},
{
"id": "churn_risk",
"calibrated_probability": 0.642,
"verdict": "no",
"calibration_tier": "roster"
}
]
}Reading a result
| Field | What it tells you |
|---|---|
calibrated_probability |
The answer. A 0–1 probability that the policy applies. This is the number to store and aggregate. |
verdict |
The model's discrete yes/no call. Useful for display, but do not aggregate verdicts into rates (see below). |
calibration_tier |
roster (a policy the model was calibrated on — accepted is meaningful) or pooled_unseen (a novel policy — probability is usable, accepted is omitted). |
accepted |
The frozen accept/defer decision. true means the confidence clears a strict, release-validated threshold. Omitted for pooled_unseen — treat a missing accepted as "no validated decision," never as false. |
evidence_ids |
Supporting message IDs, present only with include_evidence: true. An independent judgment from the matcher, so a high probability can still come back with no evidence. |
Step 3 — Estimate a rate across many conversations
To answer "what share of this week's conversations were refund requests?", score each conversation and average the calibrated probabilities for that policy across the cohort.
Warning
Average probabilities — don't count
yesverdicts. The model deliberately favors precision over recall, so countingverdict === "yes"systematically undercounts the true rate. The calibrated probabilities are built to sum correctly; the verdicts are not. This is the single most common way to misread the results.
const BLOO_API_KEY = process.env.BLOO_API_KEY;
const POLICIES = [
{ id: "refund_request", description: "The customer asks for money back or a refund." },
{ id: "churn_risk", description: "The customer signals they may cancel or stop using the product." },
];
async function assess(messages) {
const res = await fetch("https://api.blooio.com/v4/ai/bloobability/assess", {
method: "POST",
headers: {
Authorization: `Bearer ${BLOO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ conversation: messages, policies: POLICIES }),
});
if (!res.ok) throw new Error(`assess failed: ${res.status}`);
return res.json();
}
// conversations: [{ id, messages: [{ id, role, content }] }, ...]
async function policyRates(conversations) {
const totals = Object.fromEntries(POLICIES.map((p) => [p.id, 0]));
for (const convo of conversations) {
const { policies } = await assess(convo.messages);
for (const r of policies) totals[r.id] += r.calibrated_probability;
}
const n = conversations.length || 1;
return Object.fromEntries(
Object.entries(totals).map(([id, sum]) => [id, sum / n]),
);
}
// { refund_request: 0.18, churn_risk: 0.07 } — the estimated share of the cohort.Each rate is the estimated fraction of conversations in the cohort where that policy applies. Store these per bucket (day, week, agent, segment) and you have the raw material for trends.
Step 4 — Track a trend over time
Bucket your conversations by period, run policyRates on each bucket, and compare. The result is a time series of prevalence you can chart or alert on.
// buckets: [{ label: "2026-W01", conversations: [...] }, ...]
async function trend(buckets) {
const series = [];
for (const bucket of buckets) {
series.push({ label: bucket.label, rates: await policyRates(bucket.conversations) });
}
return series;
}To keep a trend meaningful, hold three things constant:
- Same policy definitions. Use the identical
idanddescriptionin every bucket. A wording change is a new measurement, not a continuation of the old one. - Comparable cohorts. Compare like with like — same channel, segment, or funnel stage — so a shift in the mix doesn't masquerade as a shift in behavior.
- One model. Don't try to re-derive or re-fit calibration yourself per period; the single frozen calibrator is what makes periods comparable.
Warning
Read magnitude as a floor, not a measurement. Reported movement is compressed — a real change tends to show up at less than its full size — so the direction of a trend is more reliable than its exact magnitude. Treat "refund rate is up" as trustworthy and "refund rate is up 4.2 points" as a conservative lower bound. For novel (
pooled_unseen) policies, trend fidelity is unmeasured entirely, so calibrate expectations and keep a human in the loop.
Step 5 — Route the hard cases to a human
For per-conversation actions (auto-tagging, auto-routing, closing tickets), use accepted as the automation gate and send everything else to review:
accepted: true→ the decision cleared the strict, validated threshold. Safe to act on automatically.accepted: falseor absent → not validated. Queue it for a human, and turn oninclude_evidence: trueso the reviewer sees theevidence_ids(the messages the model was reacting to).
accepted is intentionally strict — a confident-looking 0.88 yes still won't clear it — so plan for roughly a third to two fifths of decisions to land in review rather than automation. That split is the point: it concentrates the model's mistakes into the queue a human is already looking at.
Limitations
Warning
Know these before you build on the scores:
- Domain. Tuned for customer-support conversations in English. Other languages, other genres, and non-conversational text are unmeasured.
- Precision over recall. Misses (a policy that applied but wasn't flagged) are more likely than false alarms. This is why rates come from summed probabilities, not verdict counts.
- Calibrated, not perfect. A probability of
0.9does not mean "correct exactly nine times in ten." The probabilities are reliable in aggregate — for rates and trends — more than as a literal per-decision truth.- Model-derived labels. Scores reflect a model's judgment of your description, not human-verified ground truth. Where that judgment is systematically off, the scores will be off the same way.
Recap
- Define stable policies (
id+description) and keep them fixed over time. - Score conversations with
POST /v4/ai/bloobability/assess; readcalibrated_probability. - Estimate rates by averaging probabilities, never by counting
yesverdicts. - Track trends by bucketing over time with identical policies and comparable cohorts; trust direction over magnitude.
- Gate automation on
accepted; send the rest to review withinclude_evidence: true.
See the full field reference on the Assess a conversation endpoint page.