Call tracking

Measure every call per campaign and push the data to your tools.

Call tracking per campaign

One tracked number per ad, listing or client: the call is forwarded, the agent hears the source in a whisper, and the call with its source goes to GA4, Mixpanel and your webhook the moment it ends.

How it works

  1. Someone calls one of your tracked numbers and hears ringing.
  2. The number they dialed tells which ad, listing or client the call comes from.
  3. The call is forwarded, and the person who answers first hears the source and campaign.
  4. If nobody answers, the caller hears a short apology.
  5. When the call ends, its source, outcome and details are sent to your webhook and Mixpanel, and to Google Analytics 4 with the client ID of the website visitor, provided by your site.

Starts on: inbound-call
Actions used: javascript@v1, dial@v2, answer@v1, play@v3, hangup@v1, bridge@v3, fetch@v3, googleAnalytics@v1, mixpanel@v1, log@v1
Guide: Track Calls (Call Tracking)

# Simple call tracking.
# Buy one Callr number per ad, listing or client and assign them all to this
# scenario. Each call is forwarded to the right person, who first hears where
# the call comes from (whisper). When the call ends, the source, the outcome
# and the full CDR go to your webhook, Google Analytics 4 and Mixpanel.
#
# What to edit: the MODIFIABLE VARIABLES below ($sources first, then the
# optional webhook, GA4 and Mixpanel settings).
# Needs: nothing else to start. The optional webhook must accept requests
# from the Callr IP ranges: https://docs.callr.com/docs/ip-subnets
# GA4 (optional): the client_id must be the one of the website visitor
# (gtag('get', 'G-XXXXXXXXXX', 'client_id', callback) on your site); your
# server gives it back to this scenario, see $ga4.clientIdUrl.
# Build Mode: calls only go to your Testing Numbers.
# Docs: https://docs.callr.com/docs/call-tracking

description: "Call tracking per campaign"

compat:
  version: "2025-09-01"

defaults:
  # The caller hears ringing until the call is connected
  autoAnswer: false
  language: en-US
  voice: en-US-Olivia:MAI-Voice-2

variables:
  $dial: null
  $src: null
  $visitor: null

  # MODIFIABLE VARIABLES BELOW
  $sources: # tracked number (as called) => source name, campaign, forward to
    "+15550100101": { name: Google Ads, campaign: brand, target: "+15550100001" }
    "+15550100102": { name: Facebook, campaign: spring sale, target: "+15550100001" }
    "+15550100103": { name: Homes.com, campaign: listing A-2214, target: "+15550100002" }
  $default: { name: Unknown, campaign: Not set, target: "+15550100001" } # used for any other number
  $webhookUrl: null # e.g. https://api.example.com/call-tracking to log calls in your CRM
  # GA4 Measurement Protocol. clientIdUrl answers { "gaClientID": "..." } for
  # the number that was called: your website reads the visitor's client_id with
  # gtag('get', 'G-XXXXXXXXXX', 'client_id', callback) and stores it with the
  # tracked number it shows to that visitor (one number per visitor session).
  # Without a client_id, no GA4 event is sent: an invented one would count as
  # a new user, unrelated to the visit and the campaign.
  $ga4: { enabled: false, id: G-XXXXXXXXXX, secret: <ga4-api-secret>, clientIdUrl: <client-id-lookup-url> }
  $mixpanel: { enabled: false, username: <service-account>, secret: <service-secret>, projectId: "1234567" } # Mixpanel service account

branches:
  inbound-call:
    actions:
      # Find the source of the number that was called
      - action: javascript@v1
        params:
          code: |
            $src = $sources[call.toNumber] ?? $default;

      # Forward the call. The person who answers hears the whisper first.
      # Alternative: remove answerOnBranch for a transparent forward with no whisper.
      - action: dial@v2
        params:
          targets:
            - number: ${{ $src.target }}
              timeout: 25
          answerOnBranch: whisper
        result: $dial

      - if: "!$dial?.ok"
        then:
          - action: answer@v1
          - action: play@v3
            params:
              tts:
                text: Sorry, nobody can take your call right now. Please call back later.
          # For a voicemail fallback, see the call forwarding with voicemail template.
          - action: hangup@v1

  # Runs for the person who answers only, the caller still hears ringing
  whisper:
    actions:
      - action: play@v3
        params:
          tts:
            text: "Source: ${{ $src.name }}. Campaign: ${{ $src.campaign }}."
      - action: bridge@v3

  # Runs when the call ends, also when the caller hangs up while it rings
  hangup:
    actions:
      - if: $webhookUrl
        action: fetch@v3
        params:
          url: ${{ $webhookUrl }}
          method: POST
          headers:
            Content-Type: application/json
          body: |
            {
              "source": ${{ JSON.stringify($src?.name ?? null) }},
              "campaign": ${{ JSON.stringify($src?.campaign ?? null) }},
              "answered": ${{ JSON.stringify(!!$dial?.ok) }},
              "status": ${{ JSON.stringify($dial?.status ?? null) }},
              "run": ${{ JSON.stringify(run) }},
              "call": ${{ JSON.stringify(call) }},
              "scenario": ${{ JSON.stringify(scenario) }}
            }

      # GA4: the client_id of the website visitor who saw this number
      - if: $ga4?.enabled
        action: fetch@v3
        params:
          url: ${{ $ga4.clientIdUrl }}?number=${{ encodeURIComponent(call.toNumber) }}
          responseFormat: json
          timeout: 5
        result: $visitor

      - if: $ga4?.enabled && $visitor?.json?.gaClientID
        action: googleAnalytics@v1
        params:
          id: ${{ $ga4.id }}
          secret: ${{ $ga4.secret }}
          clientID: ${{ $visitor.json.gaClientID }}
          events:
            - name: phone_call
              params:
                source: ${{ $src?.name }}
                campaign: ${{ $src?.campaign }}
                answered: "${{ $dial?.ok ? 1 : 0 }}" # GA4 params are strings or numbers

      - if: $mixpanel?.enabled
        action: mixpanel@v1
        params:
          username: ${{ $mixpanel.username }}
          secret: ${{ $mixpanel.secret }}
          project_id: ${{ $mixpanel.projectId }}
          event: Call
          properties:
            source: ${{ $src?.name }}
            campaign: ${{ $src?.campaign }}
            answered: ${{ !!$dial?.ok }}
            talkTime: ${{ $dial?.cdr?.durationAnswered ?? 0 }}

      - action: log@v1
        params:
          message: ${{ $src?.name }} / ${{ $src?.campaign }} - ${{ $dial?.status ?? 'no answer' }}

Advanced call tracking

Everything on one tracked number: caller lookup on your server, a whisper to the agent, recording, a voicemail fallback, missed-call alerts, and Call / MissedCall events sent to GA4 and Mixpanel.

How it works

  1. When someone calls your tracked number, they hear a short welcome and a recording notice, then your server looks them up.
  2. Your agents are called one after the other; the one who answers hears the customer reference before being connected.
  3. The conversation is recorded and the transcription is sent to your server.
  4. If nobody answers, the caller can leave a voicemail and your team gets an SMS and an email with the transcription.
  5. When the call ends, the call details go to your server and a Call or MissedCall event goes to GA4 and Mixpanel.

Starts on: inbound-call
Actions used: wait@v1, play@v3, fetch@v3, javascript@v1, dial@v2, amd@v3, hangup@v1, startRecording@v2, bridge@v3, sms@v1, record@v1, gather@v4, playRecordingFile@v1, saveRecordingFile@v1, email@v1, mixpanel@v1, googleAnalytics@v1
Guide: Track Calls (Call Tracking)

# Advanced call tracking: CRM lookup, recording, GA4, Mixpanel
#
# Everything on one tracked number: the caller is looked up on your
# server, the agent hears a whisper with the customer reference, the
# call is recorded and transcribed, unanswered calls go to voicemail
# with SMS and email alerts, and Call / MissedCall events are sent
# to Google Analytics (GA4), Mixpanel and your server.
#
# What to edit: $apiUrl, $dialTargets, $alertNumbers, $reportToEmails,
# and $ga4 / $mixpanel (set enabled: true and your credentials).
# Needs: a server that answers the lookup and receives the webhooks
# (allow the Callr IP subnets: https://docs.callr.com/docs/ip-subnets).
# Build Mode: calls and SMS go only to your Testing Numbers.
# Docs: https://docs.callr.com/docs/call-tracking

description: "Advanced call tracking"

compat:
  version: "2025-09-01"

defaults:
  language: en-US
  voice: en-US-Harper:MAI-Voice-2

variables:
  $dial: null # Stores the dial result
  $voicemail: "" # Stores the voicemail file
  $menuTries: 0 # Passes through the voicemail review menu
  $customer: # Customer data, replaced by your server's answer
    id: Unknown
    name: Unidentified
    gaClientID: "" # The Google Analytics client_id property

  # MODIFIABLE VARIABLES BELOW
  $apiUrl: https://api.example.com/callr # your server (lookup and webhooks)
  $dialTargets: # the agents, called one after the other
    - number: "+15550100021"
      timeout: 20
    - number: "+33639984242"
      timeout: 20
  $alertNumbers: # receive the missed call alerts by SMS
    - "+33639984242"
  $reportToEmails: # receive the missed call alerts by email
    - [email protected]
    - [email protected]
  $ga4: # Google Analytics (GA4) tracking
    enabled: false
    propertyID: G-XXXXXXXX-Y
    measurementApiSecret: <api-secret>
  $mixpanel: # Mixpanel tracking
    enabled: false
    projectID: <project-id>
    username: <service-account-username>
    secret: <service-account-secret>

branches:
  # The 'inbound-call' branch defines actions for incoming calls
  inbound-call:
    actions:
      - action: wait@v1
        params:
          seconds: 2

      # Say hello
      - action: play@v3
        params:
          tts:
            text: Thank you for calling the Agency.

      # Please hold the line
      - action: play@v3
        params:
          tts:
            text: |
              This call may be recorded.
              Please hold the line while we connect you.

      # Send the call metadata to your server
      # And retrieve some customer data
      - action: fetch@v3
        params:
          url: ${{ $apiUrl }}/customer
          method: POST
          headers:
            Content-Type: application/json
          body: ${{ JSON.stringify(call) }}
          timeout: 3
        result: $lookup

      # Based on the call metadata (calling number, or called number),
      # your server could return some customer information (from your CRM)
      # as a JSON object: { "id": "...", "name": "...", "gaClientID": "..." }.
      # Without an answer, $customer keeps its default value.
      - if: $lookup?.ok && $lookup?.json?.id
        action: javascript@v1
        params:
          code: $customer = $lookup.json

      # Call the target numbers
      - action: dial@v2
        params:
          targets: ${{ $dialTargets }}
          answerOnBranch: targetAnswered
          parallel: false # true to ring all numbers at the same time
        result: $dial

      # If we could not reach the target numbers
      # Go to voicemail
      - if: "!$dial?.ok"
        goto:
          branch: voicemail

  # The 'targetAnswered' custom branch defines actions when one target answers
  # Those actions are played on the callee *only*
  targetAnswered:
    actions:
      # Is the callee on voicemail?
      # Answering Machine Detection
      - action: amd@v3
        result: $amd

      # If the callee (target) is on voicemail, hangup the outbound call.
      # The caller will be redirected to our own voicemail.
      - if: $amd?.detected == 'machine'
        action: hangup@v1

      # Whisper to the callee about the customer.
      # The caller still hears a ringing tone.
      - action: play@v3
        params:
          tts:
            text: This is a tracked call. Customer reference ${{ $customer.id }}.
          stopOnTalk:
            enabled: true
        result: $say

      # If there was talk during the whisper, we're probably on a voicemail.
      # Note: an agent who talks over the whisper (e.g. "Hello?") is also
      # hung up, and the caller goes to the voicemail fallback.
      - if: $say?.stoppedOnTalk
        action: hangup@v1

      # Start recording the call
      # By default, transcription is enabled.
      - action: startRecording@v2
        events:
          ready:
            # When the recording has finished processing, send the transcription
            # To your server
            # The special variable "recording" is available here
            - action: fetch@v3
              params:
                url: ${{ $apiUrl }}/transcription/${{ call.id }}
                method: POST
                headers:
                  Content-Type: application/json
                body: |
                  {
                    "call": ${{ JSON.stringify(call) }},
                    "dial": ${{ JSON.stringify($dial) }},
                    "transcription": ${{ JSON.stringify(recording.transcription) }}
                  }

      # Bridge the call caller <> callee
      - action: bridge@v3

      - action: hangup@v1

  # The 'voicemail' branch is where we send the caller when
  # the targets are not reachable
  voicemail:
    # The voicemail has a British accent
    defaults:
      language: en-GB
      voice: en-GB-Sonia:DragonHDLatest

    actions:
      # Send an SMS to the caller
      - if: call.fromType == 'MOBILE'
        action: sms@v1
        params:
          to:
            - ${{ call.fromNumber }}
          body: >
            Hello, you tried to reach us but no one was available.
            Our team will call you back as soon as possible.
          nature: ALERTING

      # We're not available.
      - action: play@v3
        params:
          tts:
            text: Sorry, we are not available at the moment.

      - label: record
        action: play@v3
        params:
          tts:
            text: Please leave a message after the tone.

      # The 'record@v1' action records the caller
      # A temporary file is created, its name is stored in the $voicemail variable
      - action: record@v1
        params:
          maxLength: 120
        result: $voicemail

      # The review menu is offered 3 times at most, then the message is kept
      - label: menu
        action: javascript@v1
        params:
          code: $menuTries++

      - if: $menuTries > 3
        action: hangup@v1

      # Gather will wait for a DTMF
      - action: gather@v4
        params:
          maxDigits: 1
          tts:
            # You can leverage YAML multi line strings
            text: |
              To listen to your message, press 1.
              To send this message, press 2.
              To re-record this message, press 3.
        result: $dtmf

      - if: $dtmf?.input == '1'
        then:
          - action: playRecordingFile@v1
            params:
              file: ${{ $voicemail }}
          - goto:
              label: menu

      - if: $dtmf?.input == '2'
        then:
          - action: play@v3
            params:
              tts:
                text: Your message has been delivered. Thank you!
          - action: hangup@v1

      - if: $dtmf?.input == '3'
        then:
          - action: javascript@v1
            params:
              code: $voicemail = ''; $menuTries = 0 # reset the message and the menu counter
          - goto:
              label: record

      # No key pressed: the message is kept and sent
      - if: $dtmf?.timeout
        action: hangup@v1

      - action: play@v3
        params:
          tts:
            text: Sorry, I did not get that.

      - goto:
          label: menu

    # with postHangup, you can define actions to run after hangup
    # on this specific branch (voicemail)
    postHangup:
      - if: $voicemail
        then:
          # Save the recording to process it.
          - action: saveRecordingFile@v1
            params:
              file: ${{ $voicemail }}
            events:
              # When the file has been processed, you can run actions on it
              ready:
                # Notify the Agency manager about the missed call
                - action: sms@v1
                  params:
                    body: |
                      You missed a call from ${{ call.fromNumber ?? '(hidden number)' }}.
                      Voicemail: "${{ transcription_leg_a }}"
                    to: ${{ $alertNumbers }}
                    nature: ALERTING

                # Notify by email too
                - action: email@v1
                  params:
                    fromName: The Agency Tracking
                    subject: Missed call from ${{ call.fromNumber ?? '(hidden number)' }}
                    text: |
                      Hello.

                      You have a new missed call from ${{ call.fromNumber ?? '(hidden number)' }}.
                      Number called: ${{ call.toNumber }} (${{ scenario.name }}).

                      Customer ID: ${{ $customer.id }}.

                      Voicemail: "${{ transcription_leg_a }}"

                      Sincerely.
                    to: ${{ $reportToEmails }}

      - if: "!$voicemail"
        then:
          - action: sms@v1
            params:
              body: |
                You missed a call from ${{ call.fromNumber ?? '(hidden number)' }}.
                No voicemail left.
              to: ${{ $alertNumbers }}
              nature: ALERTING

  # The 'hangup' branch is run when the call is hangup
  # after all specific postHangup have been run
  hangup:
    actions:
      # Push the call metadata to your server
      - action: fetch@v3
        params:
          url: ${{ $apiUrl }}/metadata/${{ call.id }}
          method: POST
          headers:
            Content-Type: application/json
          body: |
            {
              "call": ${{ JSON.stringify(call) }},
              "dial": ${{ JSON.stringify($dial) }},
              "customer": ${{ JSON.stringify($customer) }}
            }

      # The Agency missed the call
      - if: "!$dial?.ok"
        then:
          # Mixpanel (missed call)
          - if: $mixpanel.enabled
            action: mixpanel@v1
            params:
              username: ${{ $mixpanel.username }}
              secret: ${{ $mixpanel.secret }}
              project_id: ${{ $mixpanel.projectID }}
              event: MissedCall # Custom event
              properties:
                customerID: ${{ $customer.id }}
                customerName: ${{ $customer.name }}
                callingNumber: ${{ call.fromNumber }}
                calledNumber: ${{ call.toNumber }}
                talkTime: 0

          # Google Analytics (GA4) (missed call)
          - if: $ga4.enabled && !!$customer.gaClientID
            action: googleAnalytics@v1
            params:
              id: ${{ $ga4.propertyID }}
              secret: ${{ $ga4.measurementApiSecret }}
              clientID: ${{ $customer.gaClientID }}
              events:
                - name: MissedCall
                  params:
                    customerID: ${{ $customer.id }}
                    customerName: ${{ $customer.name }}
                    callingNumber: ${{ call.fromNumber }}
                    calledNumber: ${{ call.toNumber }}
                    talkTime: 0

      # If we were able to bridge the call to the Agency, push metadata
      # to external APIs
      - if: $dial?.ok
        then:
          # Mixpanel
          - if: $mixpanel.enabled
            action: mixpanel@v1
            params:
              username: ${{ $mixpanel.username }}
              secret: ${{ $mixpanel.secret }}
              project_id: ${{ $mixpanel.projectID }}
              event: Call
              properties:
                customerID: ${{ $customer.id }}
                customerName: ${{ $customer.name }}
                callingNumber: ${{ call.fromNumber }}
                calledNumber: ${{ call.toNumber }}
                talkTime: ${{ $dial?.cdr?.durationAnswered ?? 0 }}

          # Google Analytics (GA4)
          - if: $ga4.enabled && !!$customer.gaClientID
            action: googleAnalytics@v1
            params:
              id: ${{ $ga4.propertyID }}
              secret: ${{ $ga4.measurementApiSecret }}
              clientID: ${{ $customer.gaClientID }}
              events:
                - name: Call
                  params:
                    customerID: ${{ $customer.id }}
                    customerName: ${{ $customer.name }}
                    callingNumber: ${{ call.fromNumber }}
                    calledNumber: ${{ call.toNumber }}
                    talkTime: ${{ $dial?.cdr?.durationAnswered ?? 0 }}

Did this page help you?