Integrations
Start from scratch, call your API, use OAuth integrations or your own voice.
Start from scratch
An empty scenario with the three entry points, inbound call, inbound SMS and API, ready for your own actions.
How it works
- When someone calls your number, the call entry point runs.
- When someone texts your number, the SMS entry point runs.
- When your app starts the scenario by API, the API entry point runs.
- Each one only writes a log line until you add your own actions.
Starts on: inbound-call, api-initiated, inbound-sms
Actions used: log@v1
Guide: Language Reference
# An empty scenario with the three entry points.
# Keep the branches you need, delete the others,
# and replace each log@v1 with your own actions.
#
# What to edit: everything. The editor completes action names
# and parameters, and the Docs tab explains each action.
#
# Needs: a number assigned to this scenario for calls and SMS.
# In Build Mode, calls and SMS only reach your Testing Numbers.
#
# Reference: https://docs.callr.com/docs/callr-actions-reference
description: "Start from scratch"
compat:
version: "2025-09-01"
defaults:
language: en-US
branches:
# Runs when someone calls a number assigned to this scenario.
inbound-call:
actions:
- action: log@v1
params:
message: Hello inbound call
# Runs when someone texts a number assigned to this scenario.
# No audio here: send an SMS, an email or call your API.
inbound-sms:
actions:
- action: log@v1
params:
message: Hello inbound sms
# Runs when your app starts the scenario with the API.
# To place a call from here, use dial@v2 with a callerId
# and an answerOnBranch.
api-initiated:
actions:
- action: log@v1
params:
message: Hello from APIYour first call with data
The getting-started guide in one scenario: set your URL, call your number, and the full call record lands on your server as JSON the moment the call ends.
How it works
- Someone calls your number and hears a short welcome message.
- The call ends on its own after a second sentence.
- As soon as the call is over, the run, the call with its full record (durations, cost, hangup cause) and the scenario are sent to your URL as JSON.
- Your server, webhook.site, Zapier or Make receives it and can sync it to your CRM or database.
Starts on: inbound-call
Actions used: play@v3, hangup@v1, fetch@v3, googleAnalytics@v1
Guide: Your First Call with Data
# Your first call with data: a call comes in, data comes out.
# The caller hears two sentences, then the call ends and the
# hangup branch sends run, call (with the full CDR) and scenario
# to your server as JSON.
#
# What to edit: $url, the address that receives the JSON.
# A local server exposed with ngrok, a webhook.site URL,
# or a Zapier / Make "catch hook" URL all work.
#
# Needs: a number assigned to this scenario. In Build Mode, assign
# a temporary test number and call it from one of your Testing Numbers.
# Click "Intercept next call & open logs" before you call.
#
# If your server filters by IP address, allow the Callr subnets:
# https://docs.callr.com/docs/ip-subnets
#
# Step-by-step guide:
# https://docs.callr.com/docs/your-first-call-with-data
description: "Your first call with data"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Harper:MAI-Voice-2
variables:
$push: null
# MODIFIABLE VARIABLES BELOW
# the URL that receives the call data (POST, JSON)
$url: https://<your-ngrok-forwarding-url>
branches:
inbound-call:
actions:
- action: play@v3
params:
tts:
text: Welcome to my first call flow!
- action: play@v3
params:
tts:
text: I will hang up now.
- action: hangup@v1
# Runs once the call has ended: the CDR is complete here
hangup:
actions:
- action: fetch@v3
params:
url: ${{ $url }}
method: POST
headers:
content-type: application/json
body: |
{
"run": ${{ JSON.stringify(run) }},
"call": ${{ JSON.stringify(call) }},
"scenario": ${{ JSON.stringify(scenario) }}
}
# your server's answer (status, json), visible in the logs
result: $push
# Alternative: also send the call to Google Analytics (GA4).
# Your server answers { "gaClientID": "..." } with the client_id
# captured by gtag.js on your website. When you enable it, move
# the measurement id and API secret to the variables block.
# - if: $push?.json?.gaClientID
# action: googleAnalytics@v1
# params:
# id: G-XXXXXXXX-Y
# secret: measurementApiSecret
# clientID: ${{ $push.json.gaClientID }}
# events:
# - name: phone_call
# params:
# duration: ${{ call.cdr?.durationAnswered ?? 0 }}Caller lookup in your API
Your backend is queried with the caller's number while the phone rings, the caller is greeted by name, and the call data plus the full CDR can be posted to your webhook when the call ends.
How it works
- Someone calls your number and hears it ringing.
- Meanwhile, your API is asked who is calling, with the caller's number.
- The call is answered and the caller is greeted with what your API returned.
- When the call ends, the call details and the caller data are sent to your server, once you set its address.
Starts on: inbound-call
Actions used: ringing@v1, fetch@v3, answer@v1, play@v3, hangup@v1, javascript@v1
Guide: Fetch Data from Your Platform
# Looks up the caller in your API while the phone rings,
# greets them with what your API returned, then posts the call
# data and the full CDR to your server when the call ends.
# The pattern behind every CRM integration.
#
# What to edit: the lookup URL (randomuser.me is a public demo API
# that returns a fake person, the same one for the same caller's
# number: use your own), $pushUrl and $token for the end-of-call push
# (leave $pushUrl empty to skip it).
#
# Needs: a number assigned to this scenario. In Build Mode,
# calls only reach your Testing Numbers.
#
# If your server filters by IP address, allow the Callr subnets:
# https://docs.callr.com/docs/ip-subnets
#
# Guides:
# https://docs.callr.com/docs/fetch-data-from-your-platform
# https://docs.callr.com/docs/push-data-to-your-platform
description: "Caller lookup in your API"
compat:
version: "2025-09-01"
defaults:
# Do not answer yet: the caller hears ringing during the lookup
autoAnswer: false
language: en-US
voice: en-US-Harper:MAI-Voice-2
variables:
$api: null
$user: null
# MODIFIABLE VARIABLES BELOW
# receives the call data and the CDR when the call ends (POST, JSON), e.g. https://api.example.com/cdr
$pushUrl: null
# sent as "Authorization: Bearer ..." to your server
$token: <your-api-token>
branches:
inbound-call:
actions:
- action: ringing@v1
# Fetch the caller's data while the line is ringing
- action: fetch@v3
params:
# no audio feedback: the caller hears ringing
feedback:
enabled: false
# a public demo API: use your own server, e.g.
# https://api.example.com/contacts?number=${{ encodeURIComponent(call.fromNumber) }}
url: https://randomuser.me/api/?seed=${{ encodeURIComponent(call.fromNumber ?? 'anonymous') }}
timeout: 4
result: $api
- action: answer@v1
# A JSON response is decoded in $api.json.
# randomuser.me returns {"results":[{"name":{"title":"Miss","first":"Maria","last":"Davies"}, ...
- if: "!$api?.ok || !$api?.json?.results?.length"
then:
- action: play@v3
params:
tts:
text: Sorry, I have no information about you. Please try again later.
- action: hangup@v1
# Keep the first result in $user to simplify things
- action: javascript@v1
params:
code: $user = $api.json.results[0]
- action: play@v3
params:
tts:
text: Hello ${{ $user.name.title }} ${{ $user.name.first }} ${{ $user.name.last }}!
- action: play@v3
params:
tts:
text: My file says you live in ${{ $user.location.city }}, ${{ $user.location.country }}.
- action: play@v3
params:
tts:
text: See you another time!
- action: hangup@v1
# Runs after the call ends: push the call data and the fetched user
hangup:
actions:
- if: $pushUrl
action: fetch@v3
params:
url: ${{ $pushUrl }}
method: POST
headers:
Content-Type: application/json
Authorization: Bearer ${{ $token }}
body: |
{
"call": ${{ JSON.stringify(call) }},
"user": ${{ JSON.stringify($user) }}
}Salesforce caller lookup
While the phone rings, the caller is looked up in your connected Salesforce, greeted by name and announced to your rep, and every call, answered or missed, is logged as a Task on their record.
How it works
- While the phone rings, the caller's number is searched in your Salesforce Contacts and Leads.
- The call is answered and the caller is greeted by name when they are found.
- The call goes to your team, or to the record owner if you mapped owners to numbers.
- Before being connected, your rep hears who is calling and whether it is a contact or a lead.
- When the call ends, a Task is added to the Contact or Lead, marked as an inbound call or a missed call.
Starts on: inbound-call
Actions used: ringing@v1, log@v1, javascript@v1, fetch@v3, answer@v1, play@v3, dial@v2, hangup@v1, bridge@v3
Guide: OAuth Integrations
# Looks up the caller in Salesforce while the phone rings, greets them
# by name, forwards the call to your team (or to the record owner) with
# a whisper saying who is calling, and logs the call as a Task on the
# Contact or Lead when it ends ("Inbound call" or "Missed call").
#
# What to edit: $target, then the optional $owners, $countryCode,
# $trunkPrefix and $logCalls. For HubSpot, swap the Salesforce blocks
# for the commented HubSpot alternatives.
#
# Needs: a Salesforce connection in Integrations, set as the default
# one. Its user must be allowed to search Contacts and Leads and to
# create Tasks. Without a default connection, calls are still forwarded,
# with no lookup and no logging (see the run log).
# Build Mode: calls only reach your Testing Numbers.
#
# Guides:
# https://docs.callr.com/docs/oauth-integrations
# https://docs.callr.com/docs/fetch-data-from-your-platform
description: "Salesforce caller lookup"
compat:
version: "2025-09-01"
defaults:
# Do not answer yet: the caller hears ringing during the lookup
autoAnswer: false
language: en-US
voice: en-US-Harper:MAI-Voice-2
variables:
$query: null
$sf: null
$caller: null
$to: null
$dial: null
$task: null
# MODIFIABLE VARIABLES BELOW
# number that takes the calls
$target: "+15550100001"
# optional: Salesforce user ID => number, to ring the record owner instead, e.g. { "005XXXXXXXXXXXXXXX": "+15550100002" }
$owners: null
# your country calling code, to also search numbers saved in national format
$countryCode: "1"
# national prefix added in that format: "" for the US, "0" for France or the UK
$trunkPrefix: ""
# log each call as a Task in Salesforce, answered or missed
$logCalls: true
branches:
inbound-call:
actions:
- action: ringing@v1
- if: "!integrations?.defaults?.salesforce?.accessToken"
action: log@v1
params:
message: Salesforce is not connected. Connect it in Integrations and set it as the default connection.
# Search Contacts and Leads by phone while the line rings
- if: integrations?.defaults?.salesforce?.accessToken && call.fromNumber?.startsWith('+')
then:
# SOSL query: the E.164 number (the + is escaped), then the national format
- action: javascript@v1
params:
code: |
const cc = String($countryCode ?? '');
const terms = ['\\' + call.fromNumber];
if (cc && call.fromNumber.startsWith('+' + cc)) {
terms.push(($trunkPrefix ?? '') + call.fromNumber.slice(cc.length + 1));
}
return 'FIND {' + terms.join(' OR ') + '} IN PHONE FIELDS'
+ ' RETURNING Contact(Id,Name,OwnerId), Lead(Id,Name,OwnerId) LIMIT 5';
result: $query
- action: fetch@v3
params:
url: ${{ integrations.defaults.salesforce.metadata.instance_url }}/services/data/v61.0/search/?q=${{ encodeURIComponent($query) }}
headers:
Authorization: Bearer ${{ integrations.defaults.salesforce.accessToken }}
# no audio feedback: the caller hears ringing
feedback:
enabled: false
timeout: 5
result: $sf
# Keep the first match: { Id, Name, OwnerId, attributes: { type: "Contact" } }
- action: javascript@v1
params:
code: $caller = $sf?.json?.searchRecords?.[0] ?? null
# Alternative: HubSpot. Replace the two Salesforce blocks above
# (the log and the lookup) with this one. Verify before use.
# - if: integrations?.defaults?.hubspot?.accessToken && call.fromNumber?.startsWith('+')
# then:
# - action: fetch@v3
# params:
# url: https://api.hubapi.com/crm/v3/objects/contacts/search
# method: POST
# headers:
# Content-Type: application/json
# Authorization: Bearer ${{ integrations.defaults.hubspot.accessToken }}
# body: |
# {
# "filterGroups": [{ "filters": [{ "propertyName": "phone", "operator": "EQ", "value": ${{ JSON.stringify(call.fromNumber) }} }] }],
# "properties": ["firstname", "lastname", "hubspot_owner_id"],
# "limit": 1
# }
# feedback:
# enabled: false
# timeout: 5
# result: $sf
# - action: javascript@v1
# params:
# code: |
# const c = $sf?.json?.results?.[0];
# $caller = c ? {
# Id: c.id,
# Name: [c.properties?.firstname, c.properties?.lastname].filter(Boolean).join(' '),
# OwnerId: c.properties?.hubspot_owner_id,
# attributes: { type: 'Contact' }
# } : null;
# Ring the record owner when $owners knows them, else $target
- action: javascript@v1
params:
code: $to = $owners?.[$caller?.OwnerId] ?? $target
- action: answer@v1
- action: play@v3
params:
tts:
text: "Hello${{ $caller?.Name ? ' ' + $caller.Name : '' }}, thanks for calling. One moment please."
# The person who answers hears the whisper first
- action: dial@v2
params:
targets:
- number: ${{ $to }}
timeout: 30
answerOnBranch: whisper
result: $dial
- if: "!$dial?.ok"
then:
- action: play@v3
params:
tts:
text: Sorry, nobody can take your call right now. Please call back later.
- action: hangup@v1
# Runs for the person who answers only, the caller still hears ringing
whisper:
actions:
- action: play@v3
params:
tts:
text: "Call from ${{ $caller ? $caller.Name + ($caller.attributes?.type === 'Lead' ? ', a lead' : ', a contact') : 'a number not found in your CRM' }}."
- action: bridge@v3
- action: hangup@v1
# Runs when the call ends: log it as a Task on the Contact or Lead,
# with the Subject "Missed call" when nobody answered
hangup:
actions:
- if: $logCalls && $caller?.Id && integrations?.defaults?.salesforce?.accessToken
then:
- action: fetch@v3
params:
url: ${{ integrations.defaults.salesforce.metadata.instance_url }}/services/data/v61.0/sobjects/Task
method: POST
headers:
Content-Type: application/json
Authorization: Bearer ${{ integrations.defaults.salesforce.accessToken }}
body: |
{
"Subject": ${{ JSON.stringify($dial?.ok ? 'Inbound call' : 'Missed call') }},
"WhoId": ${{ JSON.stringify($caller.Id) }},
"Status": "Completed",
"TaskSubtype": "Call",
"CallType": "Inbound",
"CallDurationInSeconds": ${{ JSON.stringify($dial?.cdr?.durationAnswered ?? 0) }},
"ActivityDate": ${{ JSON.stringify(new Date(now).toISOString().slice(0, 10)) }},
"Description": ${{ JSON.stringify('From ' + call.fromNumber + ' to ' + $to + ': ' + ($dial?.status ?? 'not answered')) }}
}
timeout: 5
result: $task
- if: "!$task?.ok"
action: log@v1
params:
message: Salesforce Task not created (${{ $task?.status }}) ${{ $task?.body }}
# Alternative: HubSpot. Replace the Salesforce block above with
# this one (call engagement associated to the contact). Verify before use.
# - if: $logCalls && $caller?.Id && integrations?.defaults?.hubspot?.accessToken
# action: fetch@v3
# params:
# url: https://api.hubapi.com/crm/v3/objects/calls
# method: POST
# headers:
# Content-Type: application/json
# Authorization: Bearer ${{ integrations.defaults.hubspot.accessToken }}
# body: |
# {
# "properties": {
# "hs_timestamp": ${{ JSON.stringify(new Date(now).toISOString()) }},
# "hs_call_title": "Inbound call",
# "hs_call_direction": "INBOUND",
# "hs_call_status": "COMPLETED",
# "hs_call_from_number": ${{ JSON.stringify(call.fromNumber) }},
# "hs_call_duration": ${{ JSON.stringify(($dial?.cdr?.durationAnswered ?? 0) * 1000) }}
# },
# "associations": [{
# "to": { "id": ${{ JSON.stringify($caller.Id) }} },
# "types": [{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 194 }]
# }]
# }
# timeout: 5Your own ElevenLabs voice
Sets ElevenLabs as the text-to-speech engine for every prompt in the scenario: a building block that plays a welcome, ready for your own actions.
How it works
- Someone calls your number.
- Every message of the scenario is spoken with your ElevenLabs voice.
- The caller hears a welcome message.
- You add what happens next: a menu, a transfer, a voicemail.
Starts on: inbound-call
Actions used: play@v3, hangup@v1
Guide: Voices & Audio
# Uses your ElevenLabs account as the text-to-speech engine:
# every prompt of the scenario is spoken with your ElevenLabs voice.
# This is a building block, not a call flow: it plays a welcome
# message, then you add your own actions.
#
# What to edit: voice (an ElevenLabs voice ID from your voice
# library), config.key (your ElevenLabs API key), modelID,
# language, and the welcome text.
#
# Needs: an ElevenLabs account (synthesis is billed by ElevenLabs)
# and a number assigned to this scenario. In Build Mode,
# calls only reach your Testing Numbers.
#
# Guide: https://docs.callr.com/docs/voices-and-audio
description: "Your own ElevenLabs voice"
compat:
version: "2025-09-01"
defaults:
# eleven_flash_v2_5 is multilingual: set fr-FR, es-ES, de-DE...
language: en-US
# Your ElevenLabs voice ID
voice: your-elevenlabs-voice-id
textToSpeech:
engine: elevenlabs
config:
key: your-elevenlabs-api-key
# https://elevenlabs.io/docs/overview/models#models-overview
modelID: eleven_flash_v2_5
branches:
inbound-call:
actions:
- action: play@v3
params:
tts:
text: Hello, and welcome. Thank you for calling.
# Add your actions here: gather@v4 for a menu,
# dial@v2 to forward the call, record@v1 for a message...
- action: hangup@v1Updated about 1 hour ago
