Skip to content

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.

Open Calls

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. 1

    Send a capture with the call

    purpose says why you are calling, script is the flow to follow, details are facts about the person, and capture is what to find out. All four are optional; capture is what makes a result worth reading.

  2. 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. 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. 4

    You get the result

    Read it on GET /calls/{call_id} as result, or receive it pushed as the call.outcome webhook.

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 #

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 null

Up 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 null

Which answer counts as a success: {"field": "…", "equals": "…"}. Sets success on every result and the success rate on a campaign report. See below.

Fields #

labelstringrequired

Up 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.

keystringoptional

The 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 text

What kind of answer to expect. Decides the type of the value you get back (table below).

textyes_nochoicemulti_choicenumberratingdate
hintstringoptionaldefault null

Up 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 false

Marks the answers that matter most. The agent treats them as important, and a call cannot be completed while a required answer is missing.

displaystringoptionaldefault auto

How 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.

autobarsdonutstatlisthidden

What 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_no field, with equals set to "yes" or "no";
  • a choice or multi_choice field, with equals set to one of its options (for multi_choice, success means that option was among the answers);
  • a rating field, with equals set 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 sentiment in the result for that.
  • Prefer choice to text when 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 hint for units and meaning, not instructions: In rupees, per month, not Ask 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 #

Shell
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:

201 Created
{
"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:

200 OK — 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:

POST to your endpoint
{
"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 completed record 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 #

GET/api/v1/campaigns/{campaign_id}/outcomes
Shell
curl https://services.amomic.in/api/v1/campaigns/cmp_01JQRS4T8XZ0AB1CD2EF3GH7/outcomes \
-H "Authorization: Bearer $AMOMIC_API_KEY"
200 OK
{
"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 #

GET/api/v1/campaigns/{campaign_id}/outcomes.csv

Needs 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.

Shell
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 Yes or No; 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. summary and next_step_reason are 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 with POST /calls can't; use next_step and 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.fields and call.outcome like any other. See Outcomes on incoming calls.

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.