Call data

Record, transcribe and summarize calls with AI, then push the data to your platform.

Record and transcribe calls

Announces the recording, forwards the call, then emails the recording link and transcript as soon as the recording is ready, and posts them to your webhook if you want.

How it works

  1. When someone calls, they hear that the call may be recorded.
  2. The call rings your team and the conversation is recorded.
  3. If nobody answers, the caller hears that you are not available.
  4. As soon as the recording is ready, you get the audio link and the transcript by email, and on your webhook if you set one.

Starts on: inbound-call
Actions used: play@v3, startRecording@v2, fetch@v3, email@v1, stopRecording@v2, dial@v2, hangup@v1, bridge@v3
Guide: Record and Transcribe Calls

# Announces the recording, forwards incoming calls to your team and
# records them. When the recording and transcript are ready, they are
# emailed to you and posted to your webhook if you set one.
#
# What to edit: $dialTargets, $reportToEmails, and $url if you want
# the data on your server.
# Needs: nothing else, transcription is included.
# Build Mode: calls and SMS go only to your Testing Numbers.
# Webhook calls come from these IP ranges: https://docs.callr.com/docs/ip-subnets
# Guide: https://docs.callr.com/docs/call-recording-transcription

description: "Record and transcribe calls"

compat:
  version: "2025-09-01"

defaults:
  # language for speech synthesis and transcription
  language: en-US
  voice: en-US-Harper:MAI-Voice-2

variables:
  $dial: null

  # MODIFIABLE VARIABLES BELOW
  $url: null # to receive the data from your server, e.g., https://api.example.com/callr
  $dialTargets: # call forwarding to these numbers
    - number: "+33639984242"
      timeout: 20
    - number: "+15550100131"
      timeout: 20
  $reportToEmails: # receives the recording + transcription
    - [email protected]
    - [email protected]

branches:
  inbound-call:
    actions:
      # Announce the recording before it starts
      - action: play@v3
        params:
          tts:
            text: >
              Hello, this call may be recorded for statistical and security purposes.

      - action: startRecording@v2
        events:
          # These actions are executed when the recording and transcription are ready.
          ready:
            # Push the data to your server
            - if: $url
              action: fetch@v3
              params:
                method: POST
                url: ${{ $url }}
                headers:
                  Content-Type: application/json
                  # If your server requires a header
                  # Authorization: Bearer <secret>
                body: |
                  {
                    "run": ${{ JSON.stringify(run) }},
                    "call": ${{ JSON.stringify(call) }},
                    "scenario": ${{ JSON.stringify(scenario) }},
                    "recordingURL": ${{ JSON.stringify(recording?.url) }},
                    "transcription": ${{ JSON.stringify(recording?.transcription) }}
                  }

            - if: $reportToEmails?.length > 0
              action: email@v1
              params:
                subject: Call transcript from ${{ call.fromNumber ?? '(hidden number)' }}
                text: |
                  Hello,

                  Call from ${{ call.fromNumber ?? '(hidden number)' }}. Duration: ${{ recording?.duration }}s.

                  Audio recording: ${{ recording?.url }}

                  A = ${{ call.fromNumber ?? '(hidden number)' }}
                  B = ${{ $dial?.cdr?.callee ?? scenario.name }}

                  ${{ chat_text }}

                to: ${{ $reportToEmails }}

      # Tip: to keep sensitive data out of the recording, add
      # "- action: stopRecording@v2" right before the step that asks for it
      # (for example a gather@v4 that asks for a card number), not here:
      # here it would stop the recording before the call is forwarded.

      # Transfer the call to one or more numbers
      - action: dial@v2
        params:
          targets: ${{ $dialTargets }}
          answerOnBranch: outbound
        result: $dial

      # If no number answered, say we are not available
      - if: "!$dial?.ok"
        then:
          - action: play@v3
            params:
              tts:
                text: Sorry, we are not available at the moment.
          - action: hangup@v1

  # This branch is executed for the callee (transfer) only
  outbound:
    actions:
      - action: play@v3
        params:
          tts:
            text: >
              This call may be recorded for statistical and security purposes.
      - action: bridge@v3

AI summary of every call

AI behind the call: forwards and records each call, asks your LLM your own questions about the transcript, then emails the answers and pushes them to your webhook or CRM the moment the call ends.

How it works

  1. When someone calls, they hear that the call may be recorded.
  2. The call rings your team and the conversation is recorded.
  3. When the call ends, the transcript is sent to your AI with your questions.
  4. You get the answers, the transcript and the recording link by email, and on your webhook if you set one.

Starts on: inbound-call
Actions used: play@v3, startRecording@v2, javascript@v1, openai@v3, fetch@v3, email@v1, dial@v2, hangup@v1, bridge@v3
Guide: Extract Call Data with AI

# Forwards incoming calls to your team and records them. When the call
# ends, the transcript is sent to your LLM with your own questions
# (reason, summary, outcome...), then the answers are emailed to you
# and posted to your webhook.
#
# What to edit: $openai.key, $aiQuestions, $dialTargets, $emails,
# and $url if you want the data on your server.
# Needs: an OpenAI API key (or any LLM reachable with fetch@v3).
# Build Mode: calls and SMS go only to your Testing Numbers.
# Webhook calls come from these IP ranges: https://docs.callr.com/docs/ip-subnets
# Guide: https://docs.callr.com/docs/extract-data-with-ai

description: "AI summary of every call"

compat:
  version: "2025-09-01"

defaults:
  # language for speech synthesis and transcription
  language: en-US
  voice: en-US-Ethan:MAI-Voice-2

variables:
  $dial: null
  $aiResult: null
  $aiAnswers: {}
  $aiAnswersText: ""

  # MODIFIABLE VARIABLES BELOW
  $url: null # to receive the data from your server, e.g., https://api.example.com/callr
  $openai:
    key: <openai-key> # Your OpenAI API key
    model: gpt-5.4-mini
    temperature: 0.2
  $aiQuestions: # questions asked to the AI about each call
    - What is the reason for the call in less than 6 words?
    - What is the summary of the conversation in a few short sentences?
    - Was an appointment set up between the caller and the callee?
    - Did the caller reach voicemail?
  # Alternative: coaching questions, replace the list above with
  # $aiQuestions:
  #   - Did the agent greet the caller and introduce themselves?
  #   - Did the agent ask open questions to understand the need?
  #   - Did the agent propose a clear next step?
  #   - What could the agent do better, in one sentence?
  $dialTargets: # call forwarding to these numbers
    - number: "+33199001020"
      timeout: 20
    - number: "+33639980010"
      timeout: 20
  $emails: # receives the AI reports
    - [email protected]
    - [email protected]

branches:
  inbound-call:
    actions:
      - action: play@v3
        params:
          tts:
            text: >
              Hello, this call may be recorded for statistical and security purposes.

      - action: startRecording@v2
        events:
          ready:
            # these actions are executed when the recording
            # and transcription are ready

            # prepare the variables $text, $A, and $B for the prompt
            - action: javascript@v1
              params:
                code: |
                  let caller = call.fromNumber
                  let callee = scenario.name // call.toNumber
                  let text   = ""

                  if ($dial?.cdr?.callee) {
                    callee = $dial.cdr.callee
                  }
                  if (!caller) {
                    caller = "The caller"
                  }
                  for (const line of chat) {
                    const who = line.from === 'leg_a' ? caller : callee
                    text = text + who + ': "' + line.display + '"\r\n'
                  }

                  return {
                    $text: text,
                    $A: caller,
                    $B: callee
                  }

            # prepare the system prompt
            - action: javascript@v1
              params:
                code: |
                  const $messages = [
                    { role: 'system', content: `You are given a transcript of a conversation between "${$A}" (caller) and "${$B}" (callee). The transcript is:` },
                    { role: 'system', content: $text },
                    { role: 'system', content: `Answer the following questions strictly based on this transcript. Do not add any details that are not present. `},
                    { role: 'system', content: `If the transcript does not provide enough information for a question, answer with '-' only. Keep your answers short and concise, using only a few words or sentences as specified.`},
                    { role: 'system', content: `Format your final reply as a JSON object with properties \"Q1\", \"Q2\", \"Q3\", etc.`},
                    { role: 'user', content: '- ' + $aiQuestions.join('\n- ') }
                  ]
                  return { $messages }

            # OpenAI
            # Can be replaced with a fetch@v3 to any API
            # Mistral, Gemini, Claude, Together.AI, etc.
            - action: openai@v3
              params:
                secret: ${{ $openai.key }}
                endpoint: chat/completions
                properties:
                  model: ${{ $openai.model }}
                  messages: ${{ $messages }}
                  temperature: ${{ $openai.temperature }}
                  response_format:
                    type: json_object
                timeout: 60
              result: $aiResult

            # Save the answers in $aiAnswers ('-' if the AI did not answer)
            - action: javascript@v1
              params:
                code: |
                  $aiAnswers = {}
                  if ($aiResult?.ok && $aiResult?.choices?.length) {
                    try {
                      $aiAnswers = JSON.parse($aiResult?.choices?.[0]?.message?.content) ?? {}
                    } catch (e) {
                      $aiAnswers = {}
                    }
                  }
                  $aiAnswersText = $aiQuestions
                    .map((question, index) => {
                      const answerKey = `Q${index + 1}`;
                      return `"${question}"\r\n\t=> ${$aiAnswers?.[answerKey] ?? '-'}`;
                    })
                    .join('\r\n'); // CRLF for emails
                  if (!$aiResult?.ok) {
                    $aiAnswersText = 'AI analysis unavailable (check $openai.key)\r\n\r\n' + $aiAnswersText
                  }

            # Push the data to your server
            - if: $url
              action: fetch@v3
              params:
                method: POST
                url: ${{ $url }}
                headers:
                  Content-Type: application/json
                  # If your server requires a header
                  # Authorization: Bearer <secret>
                body: |
                  {
                    "run": ${{ JSON.stringify(run) }},
                    "call": ${{ JSON.stringify(call) }},
                    "scenario": ${{ JSON.stringify(scenario) }},
                    "recordingURL": ${{ JSON.stringify(recording?.url) }},
                    "transcription": ${{ JSON.stringify(recording?.transcription) }},
                    "chat": ${{ JSON.stringify(chat) }},
                    "ai": ${{ JSON.stringify($aiAnswers) }}
                  }

            - if: $emails?.length > 0
              action: email@v1
              params:
                subject: Call summary from ${{ call.fromNumber ?? '(hidden number)' }}
                text: |
                  Hello,

                  Call from ${{ call.fromNumber ?? '(hidden number)' }}. Duration: ${{ recording?.duration }}s.

                  Audio recording: ${{ recording?.url }}

                  AI Analysis:
                  ${{ $aiAnswersText }}

                  Transcription:
                  ${{ $text }}

                to: ${{ $emails }}

      # Transfer the call to one or more numbers
      - action: dial@v2
        params:
          targets: ${{ $dialTargets }}
          answerOnBranch: outbound
        result: $dial

      # If no number answered, say we are not available
      - if: "!$dial?.ok"
        then:
          - action: play@v3
            params:
              tts:
                text: Sorry, we are not available at the moment.
          - action: hangup@v1

  # This branch is executed for the callee (transfer) only
  outbound:
    actions:
      - action: play@v3
        params:
          tts:
            text: >
              This call may be recorded for statistical and security purposes.
      - action: bridge@v3

Click-to-call with AI summary

Connects two numbers by API, records the conversation, and emails and posts the AI-extracted answers to your webhook when the call ends.

How it works

  1. Your app starts the scenario with the two numbers to connect.
  2. Callr calls the first number, announces the recording and starts recording.
  3. Callr calls the second number, announces the recording and connects them.
  4. When the transcript is ready, your AI model answers your own questions about the call.
  5. The answers, the recording link and the transcript are emailed to your team and posted to your webhook.

Starts on: api-initiated
Actions used: dial@v2, play@v3, startRecording@v2, javascript@v1, openai@v3, fetch@v3, email@v1, hangup@v1, bridge@v3
Guide: Extract Call Data with AI

# This is an API-initiated template
# To start this scenario, you can click on "test"
# on the right bar, and then "START ACTION".
#
# To see how to start it with the API,
# click "SHOW API CODE".
#
# Click-to-call with AI: Callr calls $targetA, announces the recording, records
# the call and connects $targetB. When the recording and transcript are ready,
# your LLM answers $aiQuestions; the answers are emailed and posted to your webhook.
#
# What to edit: $openAiKey, $aiQuestions, $aiReportsToEmails, $webhookUrl, $callerId,
# $targetA / $targetB (or pass them in the API call), the announcements.
# Needs: an OpenAI API key (or any compatible LLM through fetch@v3),
# a caller ID assigned or approved on your account (shown to both numbers),
# a webhook (optional).
# Build Mode: calls only to your Testing Numbers.
# Webhook source IPs: https://docs.callr.com/docs/ip-subnets
# Docs: https://docs.callr.com/docs/extract-data-with-ai

description: "Click-to-call with AI summary"

compat:
  version: "2025-09-01"

defaults:
  # language for text-to-speech and transcription
  language: en-US
  voice: en-US-Harper:MAI-Voice-2

variables:
  $aiAnswers: []
  $dial: null
  $openai: null

  # MODIFIABLE VARIABLES BELOW
  # Your webhook, e.g. https://api.example.com/callr (null = no webhook)
  $webhookUrl: null
  # Your OpenAI API key
  $openAiKey: <openai-api-key>
  # OpenAI model used for the answers
  $openAiModel: gpt-5.4-mini
  # Temperature (0 = factual, 2 = creative)
  $openAiTemperature: 0.2
  # Questions asked to the AI about the transcript
  $aiQuestions:
    - What is the reason for the call in less than 6 words?
    - What is the summary of the conversation in a few short sentences?
    - Was an appointment scheduled between the caller and the recipient?
    - Did the caller reach a voicemail?
  # Email addresses that receive the AI answers
  $aiReportsToEmails:
    - [email protected]
    - [email protected]
  # Caller ID shown to both numbers: assigned or approved on your account
  $callerId: "+33199001234"
  # First number called (pass it in the API call)
  $targetA: "+33639984201"
  # Second number called, connected to $targetA (pass it in the API call)
  $targetB: "+33639984202"
  # Announcement played to $targetA before recording
  $whisperA: >
    Hello, this call may be recorded for statistical and security purposes.
  # Announcement played to $targetB before connecting
  $whisperB: >
    Hello, this call may be recorded for statistical and security purposes.

branches:
  api-initiated:
    actions:
      - action: dial@v2
        params:
          callerId: ${{ $callerId }}
          targets:
            - number: ${{ $targetA }}
              timeout: 20
          answerOnBranch: targetA

  # This branch is executed for $targetA only
  targetA:
    actions:
      - action: play@v3
        params:
          tts:
            text: ${{ $whisperA }}

      - action: startRecording@v2
        events:
          ready:
            # these actions are executed when recording
            # and transcription are ready

            # prepares the variables $text, $A and $B for the prompt
            - action: javascript@v1
              params:
                code: |
                  let caller = $targetA
                  let callee = $targetB
                  let text   = ""

                  for (const line of chat ?? []) {
                    const who = line.from === 'leg_a' ? caller : callee
                    text = text + who + ': "' + line.display + '"\r\n'
                  }

                  return {
                    $text: text,
                    $A: caller,
                    $B: callee
                  }

            # prepares the system prompt
            - action: javascript@v1
              params:
                code: |
                  const $messages = [
                    { role: 'system', content: `Here is a transcription between "${$A}" (caller) and "${$B}" (recipient).` },
                    { role: 'system', content: $text },
                    { role: 'system', content: `Answer the following question based on the transcription. Do not invent any details.`},
                    { role: 'system', content: 'Provide short answers.'},
                    { role: 'system', content: `If you don't know or don't have enough information, simply reply with "-".`}
                  ]
                  return { $messages }

            # finishes preparing the prompt with the user question
            - label: loop
              action: javascript@v1
              params:
                code: |
                  const $q = $aiQuestions.shift()
                  return {
                    $prompt: [ ...$messages, { role: 'user', content: $q } ],
                    $q
                  }

            # OpenAI
            # Can be replaced by a fetch@v3 to any API
            # Mistral, Gemini, Claude, Together.AI, etc.
            - action: openai@v3
              params:
                secret: ${{ $openAiKey }}
                endpoint: chat/completions
                properties:
                  model: ${{ $openAiModel }}
                  messages: ${{ $prompt }}
                  temperature: ${{ $openAiTemperature }}
                timeout: 60
              result: $openai

            # Save the AI answer in $aiAnswers ("-" when the AI call failed)
            - action: javascript@v1
              params:
                code: |
                  const answer = $openai?.ok ? $openai?.choices?.[0]?.message?.content : null
                  $aiAnswers.push(`${$q}\r\n\t => ${answer || '-'}\r\n`)

            # Loop while there are more questions
            - if: "$aiQuestions?.length > 0"
              goto:
                label: loop

            # Push the data to your server
            - if: $webhookUrl
              action: fetch@v3
              params:
                method: POST
                url: ${{ $webhookUrl }}
                headers:
                  Content-Type: application/json
                  # If your server requires a header
                  # Authorization: Bearer <secret>
                body: |
                  {
                    "run": ${{ JSON.stringify(run) }},
                    "call": ${{ JSON.stringify(call) }},
                    "recordingURL": ${{ JSON.stringify(recording?.url) }},
                    "chat": ${{ JSON.stringify(chat ?? []) }},
                    "ai": ${{ JSON.stringify($aiAnswers) }}
                  }

            - if: $aiReportsToEmails?.length > 0
              action: email@v1
              params:
                subject: Call summary between ${{ $targetA }} and ${{ $targetB }}
                text: |
                  Hello,

                  Call between ${{ $targetA }} and ${{ $targetB }}. Duration: ${{ recording?.duration }}s.

                  Audio recording: ${{ recording?.url }}

                  AI analysis:
                  ${{ $aiAnswers.join('\r\n') }}

                  Transcription:
                  ${{ $text }}

                to: ${{ $aiReportsToEmails }}

      # Calls $targetB and connects it to $targetA
      - action: dial@v2
        params:
          callerId: ${{ $callerId }}
          # Alternative: show $targetA to $targetB instead
          # ($targetA must be approved on your account): replace the line above with
          # callerId: ${{ $targetA }}
          targets:
            - number: ${{ $targetB }}
              timeout: 20
          answerOnBranch: targetB
        result: $dial

      - if: "!$dial?.ok"
        action: play@v3
        params:
          tts:
            text: Sorry, the recipient is not available.

      - action: hangup@v1

  # This branch is executed for $targetB only
  targetB:
    actions:
      - action: play@v3
        params:
          tts:
            text: ${{ $whisperB }}
      - action: bridge@v3

Did this page help you?