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
- Someone calls one of your tracked numbers and hears ringing.
- The number they dialed tells which ad, listing or client the call comes from.
- The call is forwarded, and the person who answers first hears the source and campaign.
- If nobody answers, the caller hears a short apology.
- 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
- When someone calls your tracked number, they hear a short welcome and a recording notice, then your server looks them up.
- Your agents are called one after the other; the one who answers hears the customer reference before being connected.
- The conversation is recorded and the transcription is sent to your server.
- If nobody answers, the caller can leave a voicemail and your team gets an SMS and an email with the transcription.
- 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 }}Updated about 3 hours ago
