Call outcomes
Tell a call what to find out, then read back what happened and every answer it got. One capture format for single calls and call campaigns.
Every call ends with two questions: what happened, and what did we learn? A call outcome answers both. You describe what you want to know before the call (a capture). The agent works those questions into the conversation. After the call ends, the transcript is read against your capture and the answers come back to you as a result.
The same capture format works for a single call placed with POST /calls and for a call campaign created with POST /campaigns. It is also what the console's Outcome step builds, so a capture you design in code and one a teammate designs in the console look the same.
How it works #
- 1
Send a capture with the call
purposesays why you are calling,scriptis the flow to follow,detailsare facts about the person, andcaptureis what to find out. All four are optional;captureis what makes a result worth reading. - 2
The agent asks during the call
Your capture's fields are added to the agent's brief as a list of things to find out. The agent asks for them naturally, one at a time, skips what the person has already said, and moves on when someone doesn't want to answer. It never reads the list out as a questionnaire.
- 3
The call is read after it ends
Usually within a minute of hang-up, the whole transcript is read against your capture. A field is filled only from what the person said or clearly confirmed. Anything they didn't say stays empty. Nothing is guessed.
- 4
You get the result
Read it on
GET /calls/{call_id}asresult, or receive it pushed as thecall.outcomewebhook.
A call that drops halfway still counts
Nothing is "submitted" at the end of the call, so nothing is lost when a call is cut short. If the person answered three of your five questions before the line dropped, the result has those three answers and the disposition partial.
Designing a capture #
{"goal": "Confirm the AC service visit and book a time slot","fields": [{ "label": "Visit confirmed", "type": "yes_no", "required": true },{ "label": "Time slot", "type": "choice", "options": ["Morning", "Afternoon", "Evening"] },{ "label": "Landmark", "type": "text", "hint": "Anything that helps the technician find the flat" },{ "label": "Last service experience", "type": "rating" }],"success": { "field": "visit_confirmed", "equals": "yes" }}
goalstringoptionaldefault nullUp to 400 characters. What the call is for, in one sentence. Guides the agent and decides goal_met in the result. A capture needs a goal, at least one field, or both.
fieldsarrayoptionaldefault []Up to 12 fields. The order is the order the agent covers them in.
successobjectoptionaldefault nullWhich answer counts as a success: {"field": "…", "equals": "…"}. Sets success on every result and the success rate on a campaign report. See below.
Fields #
labelstringrequiredUp to 80 characters. What you want to know, as a short name: Budget, Preferred time. The agent sees it, and so does the person reading the report.
keystringoptionalThe name the answer comes back under in result.fields. Lowercase letters, digits and underscores, starting with a letter, up to 40 characters. Leave it out and it is made from the label: Time slot becomes time_slot, and a second Time slot becomes time_slot_2. The response echoes the capture with every key filled in.
typestringoptionaldefault textWhat kind of answer to expect. Decides the type of the value you get back (table below).
textyes_nochoicemulti_choicenumberratingdatehintstringoptionaldefault nullUp to 240 characters. How to read the answer: Monthly budget in rupees. Used by the agent and by the reading.
optionsarrayoptionaldefault []For choice and multi_choice only: 2–12 options, each up to 60 characters. Duplicates that differ only in case are merged. Ignored on other types.
requiredbooleanoptionaldefault falseMarks the answers that matter most. The agent treats them as important, and a call cannot be completed while a required answer is missing.
displaystringoptionaldefault autoHow the field is charted on a campaign's outcome dashboard in the console. Has no effect on calls. A display the type can't use falls back to auto.
autobarsdonutstatlisthiddenWhat each type returns in result.fields:
type |
Value | Example |
|---|---|---|
text |
string, up to 500 characters | "Near the blue gate" |
yes_no |
boolean | true |
choice |
one of your options, exactly as you wrote it |
"Evening" |
multi_choice |
array of your options |
["Email", "WhatsApp"] |
number |
number (integer when whole) | 25000 |
rating |
integer from 1 to 5 | 4 |
date |
string, YYYY-MM-DD |
"2026-10-09" |
An answer that doesn't fit its type is left empty rather than forced. A choice answer that isn't one of your options, a rating of 7, or a date that isn't a date all count as not answered.
The success rule #
success names one field and the answer that counts as a win. It can point at:
- a
yes_nofield, withequalsset to"yes"or"no"; - a
choiceormulti_choicefield, withequalsset to one of its options (formulti_choice, success means that option was among the answers); - a
ratingfield, withequalsset to"1"to"5".
Text, number and date fields can't be success rules. Exact equality on free text or an amount is a rule that almost never matches.
result.success is true or false when the field was answered, and null when it wasn't. Don't count null as a failure: it means the call never got that far.
Writing good fields #
- Ask for facts the person can state. "Visit confirmed" works. "Customer seems happy" doesn't, because it's an opinion. Use
sentimentin the result for that. - Prefer
choicetotextwhen you'll count the answers. Five options chart cleanly; five hundred free-text answers don't. - Keep it short. Every field is one more question on a phone call. Three to five fields is a good call; twelve is a survey.
- Use
hintfor units and meaning, not instructions:In rupees, per month, notAsk them politely. - The agent is told the kind of answer each field needs: a yes or no, a number, a date, a score from 1 to 5, or one of your options. A rating is asked for as a number, because "it was good" can't be stored as one.
A capture is not a script
The fields tell the agent what to find out, not how to say it. For the flow of the call (confirm who you're speaking to, confirm the address, then offer a slot), use script. For why you're calling, use purpose.
Placing a call with a capture #
curl -X POST https://services.amomic.in/api/v1/calls \-H "Authorization: Bearer $AMOMIC_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: job-VH-4821-confirm-call" \-d '{"to": "+919876543210","purpose": "Confirm the AC service visit for job VH-4821 on Thursday and book a time slot.","script": "1. Confirm you are speaking to the customer.\n2. Confirm the service address.\n3. Offer a time slot.\n4. Ask for a landmark.","details": { "Name": "Rohit", "Job": "VH-4821", "Address": "Flat 302, Sai Krupa Residency, Baner, Pune" },"capture": {"goal": "Confirm the AC service visit and book a time slot","fields": [{ "label": "Visit confirmed", "type": "yes_no", "required": true },{ "label": "Time slot", "type": "choice", "options": ["Morning", "Afternoon", "Evening"] },{ "label": "Landmark", "type": "text" }],"success": { "field": "visit_confirmed", "equals": "yes" }},"metadata": { "job_id": "VH-4821" }}'
The four fields that shape the call:
| Field | Up to | What it does |
|---|---|---|
purpose |
500 characters | Why you're calling. Shapes the opening and stays in the agent's instructions for the whole call. |
script |
4,000 characters | The flow to follow. Covered in order, in the agent's own words, never read out. The agent answers whatever the person asks along the way, then comes back to it. |
details |
20 entries | Facts about the person, as "Label": value. Given to the agent as information, never as instructions. When you send details, the agent confirms it is speaking to the right person before it mentions any of them. |
The capture's questions are added after your script, so a long script never pushes them out. metadata is not part of the brief; it comes back to you on the call.outcome webhook.
| capture | 12 fields | What to find out, as above. |
The response is the call, still ringing, with your capture echoed back (keys filled in) and result.state set to pending:
{"call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4","status": "ringing","direction": "outbound","to": "+919876543210","from_number": "+918047261935","agent_id": null,"duration_seconds": null,"outcome": null,"summary": null,"recording_uri": null,"started_at": null,"ended_at": null,"capture": {"goal": "Confirm the AC service visit and book a time slot","fields": [{ "key": "visit_confirmed", "label": "Visit confirmed", "type": "yes_no", "hint": null, "options": [], "required": true, "display": "auto" },{ "key": "time_slot", "label": "Time slot", "type": "choice", "hint": null, "options": ["Morning", "Afternoon", "Evening"], "required": false, "display": "auto" },{ "key": "landmark", "label": "Landmark", "type": "text", "hint": null, "options": [], "required": false, "display": "auto" }],"success": { "field": "visit_confirmed", "equals": "yes" }},"result": { "state": "pending", "disposition": null, "goal_met": null, "success": null, "summary": null, "sentiment": null, "next_step": null, "next_step_reason": null, "fields": {}, "captured": 0, "expected": 0, "missing_required": [], "read_at": null }}
A capture that doesn't validate is a 400 invalid_argument and nothing is dialled. Common causes: a choice field with fewer than two options, two fields with the same key, a blank label, a success rule pointing at a field that isn't there or at a text field, or a capture too long to hand to the agent in full (long hints and options on many fields). That last one is refused rather than silently cut, because a dropped required field would mark every call partial.
Reading the result #
Call GET /calls/{call_id} any time. It returns the same object, with result filled in once the call has been read:
{"call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4","status": "completed","outcome": "caller_hangup","duration_seconds": 74,"summary": "Rohit confirmed Thursday's AC service visit and chose the evening slot. He had to go before giving a landmark.","result": {"state": "ready","disposition": "completed","goal_met": "yes","success": true,"summary": "Rohit confirmed Thursday's AC service visit and chose the evening slot. He had to go before giving a landmark.","sentiment": "positive","next_step": "none","next_step_reason": null,"fields": { "visit_confirmed": true, "time_slot": "Evening" },"captured": 2,"expected": 3,"missing_required": [],"read_at": "2026-10-14T05:41:41.512000+00:00"}}
(Other call fields are trimmed from this example.)
result fields #
| Field | Type | Notes |
|---|---|---|
state |
string | pending, ready, failed or none. See below. |
disposition |
string | null | What happened, from a fixed list (below). |
goal_met |
string | null | yes, no or unclear, judged against your goal. |
success |
boolean | null | Your success rule applied to the answers. null when there is no rule or that field wasn't answered. |
summary |
string | null | One or two plain sentences, in English, for a busy reader. |
sentiment |
string | null | positive, neutral or negative: the person's, not the agent's. |
next_step |
string | null | none, call_back, send_whatsapp or human_follow_up. The single most useful thing to do next. |
next_step_reason |
string | null | Why, in a short sentence, when next_step isn't none. |
fields |
object | Your answers, by key. A field that wasn't answered is absent, not null. |
captured |
integer | How many fields were answered. |
expected |
integer | How many fields the capture had. |
missing_required |
array | Keys of required fields that weren't answered. |
read_at |
string | null | When the reading was made. |
state #
state |
Meaning | What to do |
|---|---|---|
pending |
The call is live, or has ended and is being read. A reading that hit a temporary error is retried automatically and also shows as pending. |
Wait for the webhook, or poll again in 30–60 seconds. |
ready |
The reading is in. | Use it. |
failed |
The call couldn't be read after several attempts. | Open the call in the console and press Read this call. |
none |
This call has no reading, for example a call from before readings existed. | Nothing will arrive. |
Every finished call gets a reading, not only calls placed with a capture. Without a capture, result still has disposition, summary, goal_met (judged against purpose), sentiment and next_step; fields is empty. Calls that were never answered, or where nobody spoke, are decided without reading anything and have no sentiment.
Dispositions #
disposition is always one of these eight values:
disposition |
Meaning |
|---|---|
completed |
The purpose of the call was dealt with to the end. Never set while a required field is unanswered. |
partial |
The call got somewhere but ended before it finished: dropped, cut off, or they had to go. Some answers may be in fields. |
callback_requested |
They asked to be called again at another time. |
not_interested |
They clearly declined, or asked not to be contacted. |
wrong_person |
The person who answered isn't the person you wanted. |
hung_up_early |
They ended the call almost immediately, before anything of substance was said. |
no_conversation |
The call connected but nobody really spoke: silence, a voicemail, a phone menu. |
unanswered |
Nobody picked up. |
Two of them are decided without reading anything: unanswered when the call was never answered, and no_conversation when it connected but the person never spoke.
`outcome` and `result` are different things
outcome (top level) is how the line ended: caller_hangup, no_answer, busy. result is what the conversation achieved. A call with outcome: "caller_hangup" can be completed (they hung up after everything was done) or partial (they hung up halfway).
Getting the result pushed: call.outcome #
Subscribe an endpoint to call.outcome and every reading is delivered as it lands, usually within a minute of call.ended:
{"id": "evt_9f2c4f3a7b28d05e91c4a761","type": "call.outcome","created_at": "2026-10-14T05:41:42Z","tenant_id": "ten_01JQRS4T8XZ0AB1CD2EF3GH4","data": {"call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4","direction": "outbound","to": "+919876543210","deployment_id": "dep_01JQRS4T8XZ0AB1CD2EF3GH6","disposition": "completed","goal_met": "yes","success": true,"summary": "Rohit confirmed Thursday's AC service visit and chose the evening slot.","sentiment": "positive","next_step": "none","fields": { "visit_confirmed": true, "time_slot": "Evening" },"captured": 2,"expected": 3,"metadata": { "job_id": "VH-4821" },"event_id": "evt_01JQRS4T8XZ0AB1CD2EF3GH9"}}
metadata is the object you sent with POST /calls, so you can match the result to your own record without storing the call_id. It is only present on calls you placed through the API, and not when you used one of the reserved metadata keys. A campaign call carries campaign_id instead. Fields with no value are left out rather than sent as null, so missing_required appears only when a required answer is missing. Event reference lists every field.
A call can be read more than once (someone presses Re-read on the call in the console), and each reading sends call.outcome again. Keep the latest one for a call_id.
Call campaigns #
A voice campaign takes the same capture, plus script and call_back_incomplete, on POST /campaigns. Every call in the campaign is read against it, and each person's answers are merged across all the calls made to them:
- A first call that dropped halfway and a second call that got the rest add up to one complete record.
- A later call that reached nobody never replaces what an earlier call learned.
- A
completedrecord stays completed.
Read the campaign's results three ways:
| Endpoint | Scope | Returns |
|---|---|---|
GET /api/v1/campaigns/{campaign_id}/outcomes |
campaigns:read |
The report: totals, funnel, dispositions, a summary of every field, and a timeline. |
GET /api/v1/campaigns/{campaign_id}/recipients |
campaigns:read |
Each person's merged outcome, paginated. See Campaigns. |
GET /api/v1/campaigns/{campaign_id}/outcomes.csv |
campaigns:write |
One row per person, one column per field. The console's export. |
Both outcome endpoints work only on call campaigns. On a WhatsApp campaign they return 412 failed_precondition with details.reason: "not_a_call_campaign". An unknown campaign id is a 404. In the console, the same report is the campaign's Outcomes tab.
The outcome report #
/api/v1/campaigns/{campaign_id}/outcomescurl https://services.amomic.in/api/v1/campaigns/cmp_01JQRS4T8XZ0AB1CD2EF3GH7/outcomes \-H "Authorization: Bearer $AMOMIC_API_KEY"
{"campaign_id": "cmp_01JQRS4T8XZ0AB1CD2EF3GH7","name": "AMC Renewals October","status": "running","totals": {"recipients": 1284,"called": 1131,"answered": 742,"debriefed": 729,"completed": 488,"partial": 97,"gave_answers": 603,"success": 311,"success_rate": 0.4191,"completion_rate": 0.6577,"answer_rate": 0.6561,"avg_capture": 0.81,"unread": 13},"funnel": [{ "key": "recipients", "label": "In the audience", "value": 1284 },{ "key": "called", "label": "Called", "value": 1131 },{ "key": "answered", "label": "Picked up", "value": 742 },{ "key": "gave_answers", "label": "Gave answers", "value": 603 },{ "key": "completed", "label": "Completed", "value": 488 },{ "key": "success", "label": "Success", "value": 311 }],"dispositions": [{ "key": "completed", "label": "Completed", "count": 488 },{ "key": "partial", "label": "Partly done", "count": 97 },{ "key": "callback_requested", "label": "Asked to call back", "count": 58 },{ "key": "not_interested", "label": "Not interested", "count": 51 },{ "key": "wrong_person", "label": "Wrong person", "count": 9 },{ "key": "hung_up_early", "label": "Hung up early", "count": 17 },{ "key": "no_conversation", "label": "No conversation", "count": 9 },{ "key": "unanswered", "label": "Didn't pick up", "count": 389 },{ "key": "unread", "label": "Not read yet", "count": 13 }],"fields": [{ "key": "renew_amc", "label": "Renew AMC", "type": "yes_no", "display": "auto", "required": true, "answered": 571, "yes": 311, "no": 260 },{ "key": "preferred_plan", "label": "Preferred plan", "type": "choice", "display": "auto", "required": false, "answered": 298,"options": [ { "label": "Basic", "count": 121 }, { "label": "Comprehensive", "count": 177 } ] }],"timeline": [{ "at": "2026-10-12", "completed": 171, "partial": 33, "other": 182 },{ "at": "2026-10-13", "completed": 196, "partial": 41, "other": 197 },{ "at": "2026-10-14", "completed": 121, "partial": 23, "other": 92 }],"capture": {"goal": "Renew the customer's annual maintenance contract","fields": [{ "key": "renew_amc", "label": "Renew AMC", "type": "yes_no", "hint": null, "options": [], "required": true, "display": "auto" },{ "key": "preferred_plan", "label": "Preferred plan", "type": "choice", "hint": null, "options": ["Basic", "Comprehensive"], "required": false, "display": "auto" }],"success": { "field": "renew_amc", "equals": "yes" }}}
| Field | Notes |
|---|---|
totals.recipients, called, answered |
People in the audience, people called at least once, and people who picked up. |
totals.debriefed |
People whose calls have been read. |
totals.completed, partial |
People whose merged record has that disposition. |
totals.gave_answers |
People with at least one field answered. |
totals.success, success_rate |
People who met the success rule, and that number out of answered. Both null when the capture has no success rule. |
totals.completion_rate |
completed out of answered. |
totals.answer_rate |
answered out of called. |
totals.avg_capture |
The average share of fields answered, among people who picked up. null when there are no fields. |
totals.unread |
People who picked up but whose reading has not landed yet. |
funnel |
The same counts as steps. success is present only with a success rule. |
dispositions |
Every disposition plus unread, in a fixed order, with zero counts included. |
fields |
One summary per capture field. yes_no gives yes and no; choice and multi_choice give options with counts; number and rating give avg, min, max, median and buckets; date gives buckets per day; text gives up to 25 recent samples. |
timeline |
Readings per day, split into completed, partial and other. When everything falls within 36 hours, buckets are hourly (2026-10-14T10:00). Times are UTC. |
capture |
The capture the campaign reads against, or null. |
Rates are fractions between 0 and 1, rounded to four places. The report is computed from the recipient rows when you ask for it and may be up to 15 seconds old. Each person's own answers are not in the report: page through /recipients for those.
The CSV export #
/api/v1/campaigns/{campaign_id}/outcomes.csvNeeds campaigns:write, not read: the file carries emails and every column of the uploaded list, which the per-recipient read does not. A reporting key reads the report and the recipients instead.
curl https://services.amomic.in/api/v1/campaigns/cmp_01JQRS4T8XZ0AB1CD2EF3GH7/outcomes.csv \-H "Authorization: Bearer $AMOMIC_API_KEY" \-o amc-renewals-october-outcomes.csv
The response is text/csv; charset=utf-8 with a Content-Disposition filename made from the campaign name, such as amc-renewals-october-outcomes.csv. It starts with a byte-order mark so spreadsheet apps read Hindi names and ₹ correctly.
- One row per person in the audience, including people not called yet. Unlike the console's table, it is never cut short.
- Columns: Name, Phone, Email, Outcome, Goal met, then Success (only with a success rule), then one column per capture field by its label, then Answers captured, Summary, Next step, Call attempts, Talk time (s) and Call ID, then one
List: …column per extra column of the uploaded list. - Outcome uses the console's words ("Completed", "Didn't pick up", "Not called yet", "Being tried again"). Yes/no answers are
YesorNo; several choices are joined with;. - A cell that starts with
=,+,-or@is prefixed with'so a spreadsheet never runs it as a formula. The phone column is left as it is. - Each export is recorded on the campaign's history against the API key.
Limits and behaviour #
- Summaries are in English.
summaryandnext_step_reasonare written in English whatever language the call was in. - Only what was said. A field is filled from what the person said or clearly agreed to, never from what the agent said alone and never by picking the closest option.
- API calls are not called back automatically. A campaign can call a person back once when a call ends partway (
call_back_incomplete). A call placed withPOST /callscan't; usenext_stepand place the next call yourself. - A reading takes a little time. Usually under a minute after the call ends; longer under load. Don't hold a request open waiting for it.
- Inbound calls are read too. Every one gets a disposition, summary and next step. To ask callers questions, set them on the number's Outcomes tab in the console; their answers then arrive in
result.fieldsandcall.outcomelike any other. See Outcomes on incoming calls.
Related #
Calls
Place a call, read it back, hang it up.
Campaign outcomes in the console
Design the Outcome step and read the outcome dashboard.
Webhook events
call.outcome and the rest of the catalogue.
Campaigns API
Create a call campaign with a capture and read its report.