> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ringg.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> To search or read these docs, connect the Ringg docs MCP server at https://docs.ringg.ai/mcp (no key needed; tools search_ringg_ai and query_docs_filesystem_ringg_ai) instead of browsing pages one by one. Setup for each client: https://docs.ringg.ai/resources/mcp-server/docs-mcp-server/mcp-for-ai-assistants.md
> The dashboard labels agents "Assistants"; these docs say "agent". Both mean the same object, identified by agent_id.
> API base URL: https://prod-api.ringg.ai/ca/api/v0. Send the workspace API key in the X-API-KEY header, from server-side code only.
> Placing a call dials a real phone number and spends credits. Confirm the number with the user before calling.
> To act on a Ringg workspace (place calls, edit agents, read transcripts), use the separate Ringg workspace MCP server at https://mcp.ringg.ai/mcp: https://docs.ringg.ai/resources/mcp-server/workspace-mcp-server/overview.md

# Lesson: Hand results to your CRM

> Subscribe Ananya to All Processing Done so each finished call reaches the Acme CRM endpoint with a bearer header, then store the fields that record the promise.

export const Quiz = ({question, answers, correctFeedback, incorrectFeedback}) => {
  const [selected, setSelected] = React.useState(null);
  const [checked, setChecked] = React.useState(false);
  const quizId = React.useId();
  const isCorrect = checked && answers[selected]?.correct;
  const reset = () => {
    setSelected(null);
    setChecked(false);
  };
  const optionStyle = (answer, i) => {
    const base = {
      display: "flex",
      alignItems: "flex-start",
      gap: "0.6rem",
      padding: "0.6rem 0.8rem",
      marginTop: "0.5rem",
      borderRadius: "0.6rem",
      border: "1px solid rgba(128, 128, 128, 0.35)",
      cursor: checked ? "default" : "pointer",
      lineHeight: 1.45
    };
    if (checked && answer.correct) {
      return {
        ...base,
        borderColor: "rgba(22, 163, 74, 0.8)",
        background: "rgba(22, 163, 74, 0.12)"
      };
    }
    if (checked && selected === i && !answer.correct) {
      return {
        ...base,
        borderColor: "rgba(220, 38, 38, 0.8)",
        background: "rgba(220, 38, 38, 0.1)"
      };
    }
    if (!checked && selected === i) {
      return {
        ...base,
        borderColor: "currentColor",
        background: "rgba(128, 128, 128, 0.12)"
      };
    }
    return base;
  };
  const buttonStyle = disabled => ({
    marginTop: "0.9rem",
    padding: "0.45rem 0.9rem",
    borderRadius: "0.5rem",
    border: "1px solid rgba(128, 128, 128, 0.5)",
    background: "rgba(128, 128, 128, 0.1)",
    color: "inherit",
    font: "inherit",
    fontSize: "0.9em",
    cursor: disabled ? "not-allowed" : "pointer",
    opacity: disabled ? 0.5 : 1
  });
  return <div style={{
    marginTop: "1.5rem",
    padding: "1rem 1.1rem",
    borderRadius: "0.8rem",
    border: "1px solid rgba(128, 128, 128, 0.35)"
  }}>
      <Badge color="green">Quiz</Badge>
      <p id={quizId + "-question"} style={{
    fontWeight: 600,
    margin: "0.6rem 0 0.2rem"
  }}>
        {question}
      </p>
      <div role="radiogroup" aria-labelledby={quizId + "-question"} aria-disabled={checked}>
        {answers.map((answer, i) => <label key={i} style={optionStyle(answer, i)}>
            <input type="radio" name={"quiz-" + quizId} checked={selected === i} onChange={() => !checked && setSelected(i)} disabled={checked} style={{
    marginTop: "0.3rem",
    flexShrink: 0
  }} />
            <span>{answer.text}</span>
          </label>)}
      </div>
      {checked && <div role="status" aria-live="polite" aria-atomic="true" style={{
    marginTop: "0.9rem",
    padding: "0.7rem 0.9rem",
    borderRadius: "0.6rem",
    lineHeight: 1.5,
    background: isCorrect ? "rgba(22, 163, 74, 0.12)" : "rgba(220, 38, 38, 0.1)"
  }}>
          <strong style={{
    marginRight: "0.4rem"
  }}>{isCorrect ? "✓" : "✗"}</strong>
          {isCorrect ? correctFeedback : incorrectFeedback}
        </div>}
      {!checked ? <button type="button" onClick={() => selected !== null && setChecked(true)} disabled={selected === null} style={buttonStyle(selected === null)}>
          Check answer
        </button> : <button type="button" onClick={reset} style={buttonStyle(false)}>
          Try again
        </button>}
    </div>;
};

export const RelatedTopics = ({items}) => <div className="not-prose mt-12">
    <div className="text-sm font-medium text-gray-500 dark:text-zinc-400 mb-3">Related topics</div>
    <div className="flex flex-col gap-2.5">
      {items.map(([title, href]) => <a key={href} href={href} className="flex items-center gap-2.5 text-sm text-gray-700 hover:text-gray-950 dark:text-zinc-300 dark:hover:text-white">
          <Icon icon="file-text" size={16} color="currentColor" />
          {title}
        </a>)}
    </div>
  </div>;

**Part 7: Capstone: EMI reminders end to end · Lesson 3 of 4 · 20 minutes**

## Objective

Have Ringg send every finished call to your CRM endpoint as one request, and know which fields to store.

## The scenario

After each call, Acme's CRM needs to know, per loan account, whether the customer promised to pay and for when, with a link to the recording for disputes. In part 6 you read that from Logs by hand; from now on the CRM gets it on its own.

## Hands-on

<Frame caption="Adding an event subscription">
  <video alt="Adding an event subscription" aria-label="Adding an event subscription" controls muted playsInline className="w-full rounded-xl" style={{ aspectRatio: "16 / 10" }} preload="metadata" controlsList="nodownload noremoteplayback" poster="/media/agents-event-subscription/poster.webp">
    <source src="https://mintcdn.com/ringgai-0da73d20/zhzhS2waLZvF8qXk/media/agents-event-subscription/video.webm?fit=max&auto=format&n=zhzhS2waLZvF8qXk&q=85&s=6b496a90f077a69ac12d4c44d53e4977" type="video/webm; codecs=&#x22;av01.0.09M.08&#x22;" data-path="media/agents-event-subscription/video.webm" />
  </video>
</Frame>

<Steps>
  <Step title="Open Event Subscription">
    Open `EMI reminder - Hindi`, then **Advanced Settings → Event Subscription**. With no subscriptions yet, the **New subscription** form is already open; otherwise click **Create Subscription**.
  </Step>

  <Step title="Point it at the CRM">
    Leave **Method** on **POST** and set **Callback URL** to your endpoint; Acme's is `https://crm.acme-lending.example/webhooks/ringg`. It must use HTTPS and return a `2xx` status within 30 seconds, so store the request first and process it later ([endpoint requirements](/documentation/build-agents/customize-agent-behaviour/webhooks/setup-and-errors#endpoint-requirements)).
  </Step>

  <Step title="Pick the event">
    Under **Select Events**, tick **All Processing Done** only. It fires once the recording, transcript and both analyses are ready and carries all of them, so the CRM gets one request per call. **Call Completed** fires for every attempt, busy and unanswered included, with no analysis yet; add it, or **Call Started**, only for live progress.
  </Step>

  <Step title="Add the secret header">
    Under **Custom Headers**, click **Add Header** and enter `Authorization` with the value `Bearer <your-token>`. Ringg sends it with every request, so your server can reject requests without it. After saving, the value is masked (`sk_l***`).
  </Step>

  <Step title="Save and fire it">
    Click **Create Subscription**: "Event subscription added", and a card with the **Webhook URL**, **Events** and **Headers**. Place a test call to your phone, answer, and say you will pay on 15 October.
  </Step>
</Steps>

By API, the same subscription is one `PATCH /agent/v1` with `operation: "edit_event_subscriptions"`; see [Configure subscriptions](/documentation/build-agents/customize-agent-behaviour/webhooks/setup-and-errors#configure-subscriptions).

### The request your CRM receives

The `all_processing_completed` event for one outbound call, from [Webhook payloads](/documentation/build-agents/customize-agent-behaviour/webhooks/call-events#all-processing-completed-event). This example comes from an agent with other keys; on Ananya, `custom_args_values` holds `due_amount`, `due_date` and `loan_account`, and `client_analysis` holds `payment_promised`, `promised_date` and `objection`.

```json theme={null}
{
  "event_type": "all_processing_completed",
  "call_id": "00000000-0000-4000-8000-0000000000c4",
  "parent_call_id": null,
  "call_sid": "00000000-0000-4000-8000-0000000000c2",
  "agent_id": "00000000-0000-4000-8000-0000000000a1",
  "workspace_id": "00000000-0000-4000-8000-0000000000c3",
  "status": "completed",
  "sub_status": "ACCEPTED",
  "call_type": "outbound",
  "call_duration": 15.73,
  "called_on": "2026-08-04T12:29:13.310707Z",
  "created_at": "2026-08-04T12:29:13.315211Z",
  "to_number": "+919876543210",
  "from_number": "+919876540111",
  "custom_args_values": {
    "callee_name": "Priya",
    "emi_amount": "4500",
    "due_date": "12 August"
  },
  "bulk_list_id": null,
  "agent_name": "EMI Reminder",
  "version_id": "00000000-0000-4000-8000-0000000000c1",
  "version_slug": "v1",
  "version_description": "Initial version",
  "call_cost": 0.15,
  "agent_message_count": 3,
  "user_message_count": 2,
  "overall_latency_seconds": 0.892345,
  "first_utterance_seconds": 0.456789,
  "transcript": [
    { "bot": "Hi Priya, this is Riya from Acme Lending. Your EMI of 4,500 rupees is due on 12 August." },
    { "user": "Yes, I know. I'll pay it this week." },
    { "bot": "Thank you. Have a good day." }
  ],
  "recording_url": "https://recordings.example.com/00000000-0000-4000-8000-0000000000c4.mp3",
  "platform_analysis": {
    "key_points": [
      "Reminded the customer of the 4,500 rupee EMI due on 12 August.",
      "The customer said they will pay this week."
    ],
    "action_items": [],
    "summary": "The agent reminded Priya of her EMI due on 12 August. She confirmed she will pay this week.",
    "classification": "payment_confirmed",
    "callback_requested": false,
    "callback_requested_time": null,
    "status": "analyzed",
    "call_disconnect_reason": "agent_hangup",
    "timezone": "Asia/Kolkata"
  },
  "platform_analysis_status": "success",
  "client_analysis": {
    "payment_intent": "will_pay",
    "promised_date": "this week",
    "dispute_raised": false
  },
  "client_analysis_status": "success",
  "tool_call_logs": []
}
```

### What to store

| Field | Why |
| - | - |
| `call_id` and `event_type` | The dedupe key: the same event can arrive twice, so update rather than insert. |
| `custom_args_values.loan_account` | Which account the result belongs to. |
| `status`, `sub_status` | `completed` / `ACCEPTED` means the call connected normally; anything else has no promise to record. |
| `client_analysis` | `payment_promised`, `promised_date` and `objection`, when `client_analysis_status` is `success`. |
| `platform_analysis.summary` | One line for the relationship manager. |
| `recording_url` | The link may expire; fetch a fresh one from [Get Call Details](/api-reference/endpoint/history/get-call-details), and download the file if disputes need it. |
| `bulk_list_id` | The campaign the call belongs to; `null` for single calls. |
| `parent_call_id` | Set on a retry or callback of an earlier call; link it to that call rather than counting a second contact. |

## Checkpoint

<Check>
  The subscription card shows **POST**, your URL and `all_processing_completed`. After the test call, your server has logged one request with `event_type` `all_processing_completed`, `client_analysis_status` `success`, and `payment_promised` `true` with the `promised_date` you gave in `client_analysis`.
</Check>

## Exercise

Open the card's menu (**⋮**), click **Edit**, tick **Call Started** too, and click **Update Subscription**. Place one more test call. Expected result: your server logs a `call_started` request as the call is dialled, then the usual `all_processing_completed`. Untick **Call Started** again afterwards.

## What you learned

* One event, `all_processing_completed`, carries the whole call; the earlier events are for live progress.
* Your endpoint answers `2xx` fast and deduplicates on `call_id` plus `event_type`; retries and callbacks are separate calls that point back with `parent_call_id`.
* The promise lives in `client_analysis`, under the keys you defined in Advanced Analysis.

<Quiz
  question="Your CRM receives the same all_processing_completed request twice for one call. What do you do?"
  answers={[
{ text: "Treat the second as a retry attempt and count a second contact", correct: false },
{ text: "Update the record keyed on call_id and event_type instead of inserting another", correct: true },
{ text: "Reply with a non-200 status so Ringg stops resending", correct: false },
{ text: "Subscribe to Call Completed instead, which never repeats", correct: false },
]}
  correctFeedback="Right. The same event can arrive twice, so call_id plus event_type is the dedupe key: update the record rather than inserting another. A retry of an earlier call is a different call that points back with parent_call_id."
  incorrectFeedback="Not quite. Duplicates are expected and handled on your side: dedupe on call_id plus event_type and update the record. A retry is a separate call with parent_call_id set, and your endpoint should answer 2xx fast either way."
/>

<RelatedTopics items={[["Event Subscription", "/documentation/build-agents/customize-agent-behaviour/event-subscription"], ["Webhook payloads", "/documentation/build-agents/customize-agent-behaviour/webhooks/call-events"]]} />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.