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

Source: https://docs.amomic.in/api/call-outcomes

---

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`](/api/calls) and for a call campaign created with [`POST /campaigns`](/api/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

:::steps

:::step 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.
:::

:::step 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.
:::

:::step 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.
:::

:::step You get the result
Read it on [`GET /calls/{call_id}`](/api/calls) as `result`, or receive it pushed as the [`call.outcome`](/webhooks/events#call-outcome) webhook.
:::

:::

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

```json title="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" }
}
```

```params
[
  {"name": "goal", "type": "string", "required": false, "default": "null", "description": "Up to 400 characters. What the call is for, in one sentence. Guides the agent and decides <code>goal_met</code> in the result. A capture needs a goal, at least one field, or both."},
  {"name": "fields", "type": "array", "required": false, "default": "[]", "description": "Up to 12 fields. The order is the order the agent covers them in."},
  {"name": "success", "type": "object", "required": false, "default": "null", "description": "Which answer counts as a success: <code>{\"field\": \"…\", \"equals\": \"…\"}</code>. Sets <code>success</code> on every result and the success rate on a campaign report. See below."}
]
```

### Fields

```params
[
  {"name": "label", "type": "string", "required": true, "description": "Up to 80 characters. What you want to know, as a short name: <code>Budget</code>, <code>Preferred time</code>. The agent sees it, and so does the person reading the report."},
  {"name": "key", "type": "string", "required": false, "description": "The name the answer comes back under in <code>result.fields</code>. Lowercase letters, digits and underscores, starting with a letter, up to 40 characters. Leave it out and it is made from the label: <code>Time slot</code> becomes <code>time_slot</code>, and a second <code>Time slot</code> becomes <code>time_slot_2</code>. The response echoes the capture with every key filled in."},
  {"name": "type", "type": "string", "required": false, "default": "text", "enum": ["text", "yes_no", "choice", "multi_choice", "number", "rating", "date"], "description": "What kind of answer to expect. Decides the type of the value you get back (table below)."},
  {"name": "hint", "type": "string", "required": false, "default": "null", "description": "Up to 240 characters. How to read the answer: <code>Monthly budget in rupees</code>. Used by the agent and by the reading."},
  {"name": "options", "type": "array", "required": false, "default": "[]", "description": "For <code>choice</code> and <code>multi_choice</code> only: 2–12 options, each up to 60 characters. Duplicates that differ only in case are merged. Ignored on other types."},
  {"name": "required", "type": "boolean", "required": false, "default": "false", "description": "Marks the answers that matter most. The agent treats them as important, and a call cannot be <code>completed</code> while a required answer is missing."},
  {"name": "display", "type": "string", "required": false, "default": "auto", "enum": ["auto", "bars", "donut", "stat", "list", "hidden"], "description": "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 <code>auto</code>."}
]
```

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.

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

::: codegroup

```bash cURL
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" }
  }'
```

```javascript Node.js
const 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" },
};

const response = await fetch("https://services.amomic.in/api/v1/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AMOMIC_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `job-${job.id}-confirm-call`,
  },
  body: JSON.stringify({
    to: job.customerPhone,
    purpose: `Confirm the AC service visit for job ${job.id} 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: job.customerName, Job: job.id, Address: job.address },
    capture,
    metadata: { job_id: job.id },
  }),
});
const call = await response.json();
if (!response.ok) throw new Error(`${call.error.code}: ${call.error.message}`);
// call.capture echoes your fields with keys filled in; call.result.state is "pending".
```

```python Python
import os

import requests

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"},
}

response = requests.post(
    "https://services.amomic.in/api/v1/calls",
    headers={
        "Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}",
        "Idempotency-Key": f"job-{job.id}-confirm-call",
    },
    json={
        "to": job.customer_phone,
        "purpose": f"Confirm the AC service visit for job {job.id} on Thursday and book a time slot.",
        "script": "1. Confirm you are speaking to the customer.\n2. Confirm the service address.\n"
                  "3. Offer a time slot.\n4. Ask for a landmark.",
        "details": {"Name": job.customer_name, "Job": job.id, "Address": job.address},
        "capture": capture,
        "metadata": {"job_id": job.id},
    },
    timeout=30,
)
call = response.json()
if not response.ok:
    raise RuntimeError(f"{call['error']['code']}: {call['error']['message']}")
```

:::

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

```json title="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}`](/api/calls) any time. It returns the same object, with `result` filled in once the call has been read:

```json title="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.

::: note `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`](/webhooks/events#call-outcome) and every reading is delivered as it lands, usually within a minute of `call.ended`:

```json title="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](/api/calls#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](/webhooks/events#call-outcome) 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`](/api/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](/api/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](/campaigns/outcomes).

### The outcome report

```endpoint
GET /api/v1/campaigns/{campaign_id}/outcomes
```

::: codegroup

```bash cURL
curl https://services.amomic.in/api/v1/campaigns/cmp_01JQRS4T8XZ0AB1CD2EF3GH7/outcomes \
  -H "Authorization: Bearer $AMOMIC_API_KEY"
```

```javascript Node.js
const response = await fetch(
  `https://services.amomic.in/api/v1/campaigns/${campaignId}/outcomes`,
  { headers: { Authorization: `Bearer ${process.env.AMOMIC_API_KEY}` } },
);
const report = await response.json();
console.log(report.totals.success_rate, report.dispositions);
```

```python Python
report = requests.get(
    f"https://services.amomic.in/api/v1/campaigns/{campaign_id}/outcomes",
    headers={"Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}"},
    timeout=30,
).json()
```

:::

```json title="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`](/api/campaigns) for those.

### The CSV export

```endpoint
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.

::: codegroup

```bash cURL
curl https://services.amomic.in/api/v1/campaigns/cmp_01JQRS4T8XZ0AB1CD2EF3GH7/outcomes.csv \
  -H "Authorization: Bearer $AMOMIC_API_KEY" \
  -o amc-renewals-october-outcomes.csv
```

```javascript Node.js
import { writeFile } from "node:fs/promises";

const response = await fetch(
  `https://services.amomic.in/api/v1/campaigns/${campaignId}/outcomes.csv`,
  { headers: { Authorization: `Bearer ${process.env.AMOMIC_API_KEY}` } },
);
if (!response.ok) throw new Error((await response.json()).error.message);
await writeFile("outcomes.csv", await response.text());
```

```python Python
response = requests.get(
    f"https://services.amomic.in/api/v1/campaigns/{campaign_id}/outcomes.csv",
    headers={"Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}"},
    timeout=120,
)
response.raise_for_status()
with open("outcomes.csv", "w", encoding="utf-8-sig", newline="") as f:
    f.write(response.text)
```

:::

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](/console/call-outcomes).

## Related

::: cards

::: card Calls
[Place a call](/api/calls), read it back, hang it up.
:::

::: card Campaign outcomes in the console
[Design the Outcome step](/campaigns/outcomes) and read the outcome dashboard.
:::

::: card Webhook events
[`call.outcome`](/webhooks/events#call-outcome) and the rest of the catalogue.
:::

::: card Campaigns API
[Create a call campaign](/api/campaigns) with a capture and read its report.
:::

:::
