sphoroVOICEdocs
Markdown

Post-call analysis

When a call ends, the transcript is read and turned into two things: a short summary, and whatever typed fields you asked for. It is how a hundred calls become a hundred rows rather than a hundred transcripts nobody reads.

When it runs

call.ended      the call is over — duration, turns, end_reason
     │
     │  a few seconds
     ▼
call.analysed   summary, extracted, and the model that produced them
Wait for call.analysed, not call.ended. This is the most common integration bug against this API. Code that reads summary in a call.ended handler finds it empty every single time and concludes the summary is broken. The two events are seconds apart and both are delivered.

The summary

On by default. A short, neutral account of what happened, written onto the call record.

PATCH/v1/agents/{id}
Shell
# Off, for a line where the transcript is the record and a summary is noise.
curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_call": {"summary": false}}'
The block replaces whole. Sending {"post_call": {"extraction": […]}} without "summary": true turns the summary off, because the block you sent does not ask for one.

Extractions

A typed field the model fills from the transcript alone, written onto the call under its own name. This is the part that makes calls queryable.

Shell
curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "post_call": {
      "summary": true,
      "extraction": [
        {
          "name": "outcome",
          "type": "enum",
          "options": ["booked", "callback_requested", "not_interested", "wrong_number", "unclear"],
          "description": "How the call ended. Use booked only if a specific date and time were agreed and read back. Use unclear if the call was cut off before an outcome."
        },
        {
          "name": "agreed_to_pay",
          "type": "boolean",
          "description": "Whether the caller agreed to the payment plan. True only if they said so explicitly — a maybe, or a request to think about it, is false."
        },
        {
          "name": "callback_number",
          "type": "string",
          "description": "A telephone number the caller asked to be reached on, if they gave one that is different from the number we called. Digits only, no spaces."
        },
        {
          "name": "amount_agreed",
          "type": "number",
          "description": "The rupee amount the caller agreed to pay, if any. The number only, without a currency symbol."
        }
      ]
    }
  }'
FieldTypeDescription
name
required
stringThe key the value is stored under on the call. Letters, digits and underscores. Stable once live — your integrations read it, so renaming one is a breaking change on your side.
type
optional
"string" | "number" | "boolean" | "enum"Defaults to string.
description
required
stringWhat to look for. This is the whole of what the model reads besides the transcript, so it is the field that decides whether an extraction works. See below.
options
optional
string[]The allowed values of an enum. The model picks one, or leaves the field empty when none applies.
model
optional
stringOn the post_call block rather than the field. Overrides the model the pass runs on. The default is a small fast one — this is a reading job, not a writing one.

Writing a description that answers reliably

The single highest-leverage thing on this page. A description is an instruction to somebody who has the transcript and nothing else — not a column header.

Instead ofWrite
"Outcome""How the call ended. Use booked only if a specific date and time were agreed and read back."
"Did they agree?""Whether the caller agreed to the payment plan. True only if they said so explicitly — a maybe is false."
"Callback number""A number the caller asked to be reached on, if different from the number we called. Digits only, no spaces."
"Budget""The monthly budget the caller stated, as a number in rupees, without a currency symbol. Leave empty if they gave a range."

Three rules that carry most of the value:

  • Say what the edge cases resolve to. "A maybe is false" removes an entire class of disagreement between two people reading the same call.
  • Say the format for anything you will parse. "Digits only, no spaces" is the difference between a number you can dial and "+91 98765 43210 (mobile)".
  • Add an "unclear" option to every enum. Without one, a call that was cut off early either gets an empty field or gets the model's best guess, and you cannot tell which happened.

Test it without placing a call

Paste a transcript in and see what the extractions produce. This is how you find out that a field never fires — before thirty calls have gone out with it.

POST/v1/agents/{id}/post_call/test
Shell
curl -s -X POST https://voice.sphoro.com/v1/agents/$AGENT_ID/post_call/test \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transcript": [
      {"role": "agent",  "text": "Hello, this is Acme about your outstanding balance of 4,200 rupees."},
      {"role": "user",   "text": "I can do two thousand now and the rest next month."},
      {"role": "agent",  "text": "That works. So two thousand today and the balance on the fifteenth?"},
      {"role": "user",   "text": "Yes, that is fine."}
    ]
  }'
Response200 OK
{
  "summary": "The caller agreed to a split payment: 2,000 rupees now and the remaining balance on the fifteenth.",
  "extracted": {
    "outcome": "booked",
    "agreed_to_pay": true,
    "amount_agreed": 2000
  },
  "model": "claude-haiku-4-5"
}
Test the calls you are worried about, not the clean one. Paste in the transcript where the caller changed their mind, the one that was cut off after ten seconds, and the one where they said "yeah, maybe". A description that handles those three handles production.

Reading the results

On the call record, once call.analysed has fired:

GET/v1/calls/{id}
Shell
curl -s https://voice.sphoro.com/v1/calls/$CALL_ID \
  -H "Authorization: Bearer $SPHORO_API_KEY"
Response200 OK
{
  "id": "call_8b21f4c9a07e3d15",
  "status": "completed",
  "end_reason": "ended_by_api",
  "duration_seconds": 96,
  "summary": "The caller agreed to a split payment…",
  "extracted": {
    "outcome": "booked",
    "agreed_to_pay": true,
    "amount_agreed": 2000,
    "callback_number": ""
  }
}

Or as the webhook body, which is what you actually want in production:

JSON
{
  "id": "evt_2f9c…",
  "type": "call.analysed",
  "call_id": "call_8b21f4c9a07e3d15",
  "created_at": "2026-09-08T09:31:08Z",
  "data": {
    "summary": "The caller agreed to a split payment…",
    "extracted": { "outcome": "booked", "agreed_to_pay": true, "amount_agreed": 2000 },
    "model": "claude-haiku-4-5"
  }
}

An empty field is a real answer

A field the model could not fill comes back empty rather than guessed. Treat that as information: it usually means the call did not cover the thing, not that extraction failed.

You seeUsually
One field empty on most callsThe conversation does not reliably reach it. Either the prompt does not ask, or the field is measuring something the call is not about.
Every field empty on one callThe call was too short to analyse — check duration_seconds and end_reason.
An enum that always picks the same optionThe options overlap, or the description does not distinguish them. Test three real transcripts against it.
A number arriving as textThe description did not say "the number only, without a currency symbol".

Extraction or function?

ExtractionFunction
RunsAfter the callDuring it
Can affect the conversationNoYes
Costs the caller timeNoneA round trip they wait through
Right for"What was the outcome", "did they agree", "what number did they give""What is this caller's balance"

Most things people build a function for turn out to be extractions. If the answer does not have to be spoken back during the call, it belongs here — it is cheaper, it cannot fail the call, and it is retried on your side rather than on the caller's.

What happens to the work afterwards

Extractions produce values; workflows act on them. Booking the slot, updating the CRM, sending the confirmation email — that is the step after this one, and it runs off the same finished call.