Call Review API · Contents
Introduction
The Call Review API scores customer calls against your KPIs and runs your own analysis prompts on them. You send a transcript and say which KPIs and prompts to run. For each KPI, the model reads the whole call and returns a score, a tier, the exact quotes it relied on, its reasoning and one improvement.
Every request and response is JSON over HTTPS. Times are UTC in ISO 8601. The model runs on a dedicated machine, and nothing you send leaves it.
An analysis runs in the background: create it, then read it back when it's done, or let us post it to your server. Try it in the Playground with your token.
https://talksens-ai.tailfb3537.ts.net# 1. Send a call with the KPIs to score
curl https://talksens-ai.tailfb3537.ts.net/v1/analyses \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"transcript": "Agent: ...\nCustomer: ...",
"kpi_ids": ["kpi_3f2a9c1d0b7e4a5f6c8d"]}'
# 2. Read it back when status is "done"
curl https://talksens-ai.tailfb3537.ts.net/v1/analyses/an_9b1c0d2e3f4a5b6c7d8e \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
Authentication
The API uses bearer tokens. Send yours in the Authorization header of every request except Check health.
Keep the token on your servers: never in a mobile app, a browser or source control. Load it from an environment variable or a secret store.
Each token comes with a webhook secret, used only to check that a callback really came from us. Both are given to you once, privately. Each team has its own token, and a token sees only its own KPIs and analyses. If a token leaks, tell us: we revoke it and it stops working at once.
Authorization: Bearer YOUR_TOKEN
Errors
Errors use standard HTTP status codes and always have the same body: an error object with a machine-readable code, a human message, and for invalid bodies a list of details.
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | No token, a wrong one, or a revoked one. |
| 404 | not_found | No such KPI or analysis for your token. |
| 409 | not_cancellable | The analysis already finished. |
| 411 | length_required | A body was sent without Content-Length. |
| 413 | too_large | The body is over 1 MB, or the transcript is over 200,000 characters. |
| 422 | invalid_request | The body doesn't match the parameters; details names the field. |
| 429 | rate_limited | More than 120 requests in a minute. Retry with backoff. |
| 429 | queue_full | 500 of your analyses are already waiting. |
| 5xx | A problem on our side. Retry with backoff; tell us if it lasts. |
{
"error": {
"code": "invalid_request",
"message": "the request body is not valid",
"details": [
{
"field": "transcript",
"problem": "Value error, send exactly one of transcript or turns"
}
]
}
}
Rate limits and timing
The model works on one call at a time and runs that call's KPIs and prompts one after another. Results are saved as each one finishes, so you can read a running analysis and see what's done so far.
| Limit | Value |
|---|---|
| Time per KPI | About 70–120 seconds, depending on the call's length |
| Requests per token | 120 per minute |
| Waiting analyses | 500 per client |
| KPIs and prompts per analysis | 20 |
| Request body | 1 MB |
| Transcript | 200,000 characters. The model reads about 32,000 tokens at once: transcript, KPI and answer together. |
| Transcripts kept | 30 days after the analysis finishes. Results are kept. |
Polling every 15–30 seconds is plenty. A queued analysis tells you its position and an estimated_wait_seconds.
Create analysis
Queues one call to be scored against KPIs and analysed with your prompts. It returns at once; the work happens in the background.
Request body
The call as text, for example Agent: … and Customer: … lines. Send either transcript or turns.
The call as a list of turns. Written for the model as [time] Speaker: text lines.
Show properties
Who is speaking, for example Agent or Customer.
What they said.
When it was said, for example 00:01:12.
Ids of KPIs you registered with Create KPI.
One-off KPIs for this call only, in the same shape as the Create KPI body. They are not saved.
Analysis prompts: any other question about the call.
Show properties
What to do with the call.
Your label for the result. Defaults to analysis.
Defaults to "text". With "json", the output is parsed JSON.
A JSON Schema the JSON output must follow.
Shorthand for one text prompt named analysis.
Anything you want back with the result, such as your call id or agent id. Up to 10 KB.
A public https address we post the finished analysis to. See Callbacks.
Send at least one KPI or prompt, and at most 20 in total.
Returns
A short analysis object with status: "queued", its place in the queue and the expected wait.
curl https://talksens-ai.tailfb3537.ts.net/v1/analyses \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"transcript": "Agent: Thank you for calling...\nCustomer: ...",
"kpi_ids": ["kpi_3f2a9c1d0b7e4a5f6c8d"],
"prompts": [{
"name": "tags",
"instructions": "Give the call reason and outcome.",
"response_format": "json"
}],
"metadata": {"call_id": "C-10293"}
}'
import os, requests
BASE = "https://talksens-ai.tailfb3537.ts.net"
HEAD = {"Authorization": f"Bearer {os.environ['CALL_REVIEW_TOKEN']}"}
job = requests.post(f"{BASE}/v1/analyses", headers=HEAD, json={
"transcript": open("call-10293.txt").read(),
"kpi_ids": ["kpi_3f2a9c1d0b7e4a5f6c8d"],
"prompts": [{"name": "tags",
"instructions": "Give the call reason and outcome.",
"response_format": "json"}],
"metadata": {"call_id": "C-10293"},
}).json()
print(job["id"], job["status"], job["position"])
const BASE = "https://talksens-ai.tailfb3537.ts.net";
const res = await fetch(`${BASE}/v1/analyses`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CALL_REVIEW_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
transcript,
kpi_ids: ["kpi_3f2a9c1d0b7e4a5f6c8d"],
prompts: [{ name: "tags", instructions: "Give the call reason and outcome.", response_format: "json" }],
metadata: { call_id: "C-10293" },
}),
});
const job = await res.json();
{
"id": "an_9b1c0d2e3f4a5b6c7d8e",
"status": "queued",
"position": 2,
"estimated_wait_seconds": 330,
"items": [
{"type": "kpi", "name": "Upselling", "kpi_id": "kpi_3f2a9c1d0b7e4a5f6c8d"},
{"type": "prompt", "name": "tags"}
],
"created_at": "2026-10-26T09:20:11Z"
}
Retrieve analysis
Returns an analysis with its status and every result finished so far.
Path parameters
The id from Create analysis.
Returns
The analysis object.
curl https://talksens-ai.tailfb3537.ts.net/v1/analyses/an_9b1c0d2e3f4a5b6c7d8e \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
import time
while True:
a = requests.get(f"{BASE}/v1/analyses/{job['id']}", headers=HEAD).json()
if a["status"] in ("done", "failed", "cancelled"):
break
time.sleep(20)
for r in a["results"]:
print(r["name"], r.get("score"), r.get("tier"))
let a;
do {
await new Promise((r) => setTimeout(r, 20000));
a = await (await fetch(`${BASE}/v1/analyses/${job.id}`, {
headers: { Authorization: `Bearer ${process.env.CALL_REVIEW_TOKEN}` },
})).json();
} while (a.status === "queued" || a.status === "running");
{
"id": "an_9b1c0d2e3f4a5b6c7d8e",
"status": "done",
"model": "talksens-1.1",
"created_at": "2026-10-26T09:20:11Z",
"started_at": "2026-10-26T09:23:40Z",
"finished_at": "2026-10-26T09:25:31Z",
"metadata": {"call_id": "C-10293"},
"items": [ ... ],
"results": [
{
"type": "kpi",
"name": "Upselling",
"kpi_id": "kpi_3f2a9c1d0b7e4a5f6c8d",
"status": "done",
"score": 8,
"undefined": false,
"tier": "Strong",
"key_evidence": ["Now that we've sorted out your issue, ..."],
"reasoning": "The agent resolved the issue first, then ...",
"improvement_suggestion": "Ask one question about usage ...",
"checks": { ... },
"timing": {"model": "talksens-1.1", "seconds": 74.2, "prompt_tokens": 1630, "output_tokens": 1162, "stop": "stop"},
"raw": "1. Score: 8\n2. Tier: Strong\n..."
}
],
"error": null
}
List analyses
Your recent analyses, newest first. Each entry is a short summary; read one with Retrieve analysis.
Query parameters
Only analyses in this status: queued, running, done, failed or cancelled.
How many to return, 1–200. Defaults to 50.
Returns
An object with an analyses list.
curl "https://talksens-ai.tailfb3537.ts.net/v1/analyses?status=done&limit=20" \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
{
"analyses": [
{
"id": "an_9b1c0d2e3f4a5b6c7d8e",
"status": "done",
"created_at": "2026-10-26T09:20:11Z",
"finished_at": "2026-10-26T09:25:31Z",
"metadata": {"call_id": "C-10293"},
"items": 2
}
]
}
Cancel analysis
Cancels a queued or running analysis. Results that already finished are kept. A finished analysis returns 409 not_cancellable.
Path parameters
The analysis to cancel.
Returns
The id and status: "cancelled".
curl -X DELETE https://talksens-ai.tailfb3537.ts.net/v1/analyses/an_9b1c0d2e3f4a5b6c7d8e \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
{"id": "an_9b1c0d2e3f4a5b6c7d8e", "status": "cancelled"}
The analysis object
One call and everything run on it.
Starts with an_.
queued, running, done, failed or cancelled. An analysis is done when at least one item finished; failed means every item failed.
The model that reads your calls: talksens-1.1.
Only while queued: its place in line, counting the analysis that's running.
Only while queued: a rough wait, at about 110 seconds per KPI ahead of it.
What was asked, in order: type (kpi or prompt), name, and kpi_id for saved KPIs.
One KPI result or prompt result per item, in the same order, added as each finishes.
What you sent.
If you gave a callback_url: the url and its delivery status.
UTC times, or null until they happen.
Set only when the whole analysis failed.
{
"id": "an_9b1c0d2e3f4a5b6c7d8e",
"status": "running",
"model": "talksens-1.1",
"created_at": "2026-10-26T09:20:11Z",
"started_at": "2026-10-26T09:23:40Z",
"finished_at": null,
"metadata": {"call_id": "C-10293"},
"items": [
{"type": "kpi", "name": "Upselling", "kpi_id": "kpi_3f2a…"},
{"type": "prompt", "name": "tags"}
],
"results": [ /* the first item, finished */ ],
"callback": {"url": "https://api.example.com/hooks", "status": null},
"error": null
}
The KPI result object
One KPI scored on one call. The fields are read from the model's answer, and every quote is checked against the transcript.
The score on the KPI's scale, or null when the KPI can't be judged from this call.
true when the model answered "undefined".
Exceptional, Strong, Adequate, Below Expectations or Critical Failure.
The quotes the score rests on.
Why this tier.
One specific thing the employee could do better.
What we verified without a judge.
Show properties
Each quote with found_in_transcript, matched word for word with case and punctuation aside.
Quotes that are not in the transcript. Treat a result that has any with care.
Whether the score falls inside its tier's band, for 1–10 KPIs.
Whether the score is inside the KPI's scale.
Whether the answer had all five parts.
model, seconds, prompt_tokens, output_tokens and stop.
The model's full answer, for your records.
done, or failed with an error object.
{
"type": "kpi",
"name": "Upselling",
"kpi_id": "kpi_3f2a9c1d0b7e4a5f6c8d",
"status": "done",
"score": 8,
"undefined": false,
"tier": "Strong",
"key_evidence": [
"Now that we've sorted out your issue, would you be interested in a faster plan?",
"I've reset your connection and it should be stable now."
],
"reasoning": "The agent resolved the issue first, then tied the upgrade to the customer's need.",
"improvement_suggestion": "Ask one question about usage before naming the plan.",
"checks": {
"all_sections_present": true,
"score_in_scale": true,
"score_fits_tier": true,
"quotes": [
{"quote": "Now that we've sorted out your issue, ...", "found_in_transcript": true}
],
"quotes_not_found": []
},
"timing": {"model": "talksens-1.1", "seconds": 74.2, "prompt_tokens": 1630, "output_tokens": 1162, "stop": "stop"},
"raw": "1. Score: 8\n2. Tier: Strong\n3. Key Evidence: ..."
}
The prompt result object
One analysis prompt run on one call.
The prompt's name.
The answer as text, or as parsed JSON for response_format: "json" (null if the model didn't give valid JSON).
For JSON prompts: whether the output parsed.
As in the KPI result object.
{
"type": "prompt",
"name": "tags",
"status": "done",
"output": {"reason": "Billing inquiry", "outcome": "Late fee waived"},
"checks": {"valid_json": true},
"timing": {"model": "talksens-1.1", "seconds": 17.0, "prompt_tokens": 702, "output_tokens": 211, "stop": "stop"},
"raw": "{\"reason\": \"Billing inquiry\", ...}"
}
Create KPI
Saves a KPI so you can run it on calls by id. See Writing a KPI.
Request body
Up to 200 characters.
The full evaluator prompt, up to 50,000 characters.
min and max. Defaults to {"min": 1, "max": 10}; used to check scores are in range.
Returns
The KPI object, with status 201.
curl https://talksens-ai.tailfb3537.ts.net/v1/kpis \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Upselling",
"instructions": "You are an evaluator analyzing a transcript for ..."}'
kpi = requests.post(f"{BASE}/v1/kpis", headers=HEAD, json={
"name": "Upselling",
"instructions": open("kpi-upselling.md").read(),
}).json()
print(kpi["id"])
const kpi = await (await fetch(`${BASE}/v1/kpis`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CALL_REVIEW_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Upselling", instructions: kpiText }),
})).json();
{
"id": "kpi_3f2a9c1d0b7e4a5f6c8d",
"name": "Upselling",
"instructions": "You are an evaluator analyzing a transcript for ...",
"scale": {"min": 1, "max": 10},
"created_at": "2026-10-26T09:14:02Z",
"updated_at": "2026-10-26T09:14:02Z"
}
List KPIs
Your saved KPIs, oldest first.
Returns
An object with a kpis list of KPI objects.
curl https://talksens-ai.tailfb3537.ts.net/v1/kpis \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
{"kpis": [{"id": "kpi_3f2a9c1d0b7e4a5f6c8d", "name": "Upselling", ...}]}
Retrieve KPI
Returns one saved KPI.
Path parameters
The KPI's id.
Returns
The KPI object.
curl https://talksens-ai.tailfb3537.ts.net/v1/kpis/kpi_3f2a9c1d0b7e4a5f6c8d \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
Update KPI
Replaces a KPI's name, instructions and scale. The new version applies to analyses created after the change; an analysis already created keeps the KPI as it was.
Request body
The same fields as Create KPI.
Returns
The updated KPI object.
curl -X PUT https://talksens-ai.tailfb3537.ts.net/v1/kpis/kpi_3f2a9c1d0b7e4a5f6c8d \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Upselling v2", "instructions": "..."}'
Delete KPI
Deletes a saved KPI. Past analyses that used it keep their results.
Returns
The id and deleted: true.
curl -X DELETE https://talksens-ai.tailfb3537.ts.net/v1/kpis/kpi_3f2a9c1d0b7e4a5f6c8d \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
{"id": "kpi_3f2a9c1d0b7e4a5f6c8d", "deleted": true}
The KPI object
An evaluator prompt you run on calls.
Starts with kpi_.
Your name for it.
The evaluator prompt.
min and max.
UTC times.
{
"id": "kpi_3f2a9c1d0b7e4a5f6c8d",
"name": "Upselling",
"instructions": "You are an evaluator analyzing a transcript for ...",
"scale": {"min": 1, "max": 10},
"created_at": "2026-10-26T09:14:02Z",
"updated_at": "2026-10-26T09:14:02Z"
}
Callbacks
Give an analysis a callback_url and we POST it there when it finishes: the same JSON as Retrieve analysis, up to three tries. The address must be public https.
Check the signature before you trust the body. Each callback carries these headers:
The analysis id.
Unix seconds when it was sent.
sha256= followed by the hex HMAC-SHA256 of timestamp + "." + raw body, keyed with your webhook secret.
Answer with any 2xx quickly. If every try fails, the result is still there to read.
import hmac, hashlib, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers["X-CallReview-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + mac,
headers["X-CallReview-Signature"])
import crypto from "node:crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-callreview-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac("sha256", secret)
.update(`${ts}.`).update(rawBody).digest("hex");
const want = Buffer.from(`sha256=${mac}`);
const got = Buffer.from(headers["x-callreview-signature"] || "");
return want.length === got.length && crypto.timingSafeEqual(want, got);
}
List samples
The sample call and KPIs the Playground offers. They are your own material, so they are only returned to a valid token.
Returns
An object with transcripts (id, name, text) and kpis (id, name, instructions, scale).
curl https://talksens-ai.tailfb3537.ts.net/v1/samples \
-H "Authorization: Bearer $CALL_REVIEW_TOKEN"
Check health
Whether the service and the model are up, and how many analyses are waiting. Needs no token.
{
"status": "ok",
"version": "1.0.0",
"model": "talksens-1.1",
"model_loaded": true,
"queue": {"queued": 0, "running": 0}
}
Writing a KPI
A KPI's instructions are the model's brief, and the call follows as the transcript. Results are read from the model's answer, so end every KPI with the output block on the right, word for word.
- Keep each KPI to one behaviour. Three focused KPIs score better than one broad one.
- Describe each score band with example phrases, and list positive and negative signals.
- Ask for exact quotes. We check each one against the transcript and flag any that aren't there.
- Say when to answer
"undefined": calls where the behaviour never comes up. - Judge the employee, not the outcome. Don't penalise them for policy, systems or the customer's own behaviour.
- Label the speakers in the transcript (
Agent:andCustomer:), or sendturns.
OUTPUT:
1. Score: [1-10 or "undefined"]
2. Tier: [Exceptional / Strong / Adequate / Below Expectations / Critical Failure]
3. Key Evidence: [3-5 exact transcript quotes that most influenced the score]
4. Reasoning: [2-3 sentences explaining why this tier was chosen]
5. Improvement Suggestion: [1 specific, actionable recommendation]
Return only the structured output above. Do not add any additional commentary, lines, or formatting.