Connect people
Click-to-call, conference rooms and number masking.
Click-to-call
Started from your app by API: calls a first number with voicemail detection, then a second number who presses 1 to accept, connects them and posts both legs' status to your webhook.
How it works
- Your app starts the scenario with the two numbers to connect.
- Callr calls the first number; if a voicemail answers, it leaves your message and stops.
- The first person presses 1 to be put in touch.
- Callr calls the second number, who presses 1 to accept, and the two are connected.
- When the call ends, the status of both calls is posted to your webhook.
Starts on: api-initiated
Actions used: javascript@v1, dial@v2, fetch@v3, amd@v3, waitForSilence@v1, play@v3, hangup@v1, gather@v4, bridge@v3
Guide: Connect Two Numbers (Click-to-Call)
# 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: your app starts the scenario, Callr calls $targetA
# (voicemail detection, press 1 to talk to an agent), then calls $targetB
# (press 1 to accept) and connects them. Both leg statuses are posted to your webhook.
#
# What to edit: $targetA / $targetB (or pass them in the API call),
# $cliA / $cliB, the messages, $webhookUrl.
# Needs: two caller IDs assigned or approved on your account, 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/connect-two-numbers
description: "Click-to-call"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Harper:MAI-Voice-2
# Every variable can be overridden for each run started by API
variables:
$dialA: null
$dialB: null
$amd: null
$dtmf: null
$body: null
$count: 0
# Status of each leg, posted to your webhook
$statusA: "nodial"
$statusB: "nodial"
# MODIFIABLE VARIABLES BELOW
# Your own ID for this run, posted back to your webhook
$customID: 0
# First number called (detection of answering machines)
$targetA: "+33639984241"
# Second number called, connected to $targetA
$targetB: "+33639984242"
# Caller ID shown to $targetA: a number assigned or approved on your account
$cliA: "+33199001234"
# Caller ID shown to $targetB: a number assigned or approved on your account
$cliB: "+33199006789"
# Receives the leg statuses when the call ends, e.g. https://api.example.com/callr/c2c
$webhookUrl: null
# Message left on $targetA's voicemail when detected
$messageOnVoicemail: >
Hello! We tried to reach you but got your voicemail.
We will try again later.
branches:
api-initiated:
actions:
# Dial A
- action: javascript@v1
params:
code: |
$statusA = 'dialing'
# The dial action is blocking until the call is hung up
- action: dial@v2
params:
callerId: ${{ $cliA }}
targets:
- number: ${{ $targetA }}
timeout: 30
answerOnBranch: answered
result: $dialA
- if: $statusA === 'dialing'
action: javascript@v1
params:
code: $statusA = $dialA?.status
# Push the call statuses to your server
- if: $webhookUrl
then:
- action: javascript@v1
params:
code: |
return {
customID: $customID,
run: run,
dial: {
A: $dialA,
B: $dialB
},
status: {
A: $statusA,
B: $statusB
}
}
result: $body
- action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
body: ${{ JSON.stringify($body) }}
headers:
Content-Type: application/json
# The answered branch defines the actions for the first leg of the call (A)
answered:
actions:
# Let's detect if we're on a voicemail
- action: amd@v3
params:
timeout: 4
result: $amd
# If we detected a voicemail, leave a message and hang up
- if: $amd?.detected === 'machine'
then:
- action: javascript@v1
params:
code: $statusA = 'voicemail_detected'
# Wait for silence after the "beep"
- action: waitForSilence@v1
params:
silence: 2.5
iterations: 3
# Leave the message on voicemail
- action: play@v3
params:
tts:
text: ${{ $messageOnVoicemail }}
# Update the leg status
- action: javascript@v1
params:
code: $statusA = 'message_left_on_voicemail'
- action: hangup@v1
# No voicemail detected: the call is answered
- action: javascript@v1
params:
code: $statusA = 'answered'
# Ask for confirmation
- label: ask
action: gather@v4
params:
maxDigits: 1
tts:
text: Hello! To talk to an agent, press 1.
result: $dtmf
- if: $dtmf?.input === '1'
goto:
branch: dialLegB
# Up to 3 attempts
- action: javascript@v1
params:
code: $count++
- if: $count >= 3
action: hangup@v1
- goto:
label: ask
dialLegB:
actions:
# Update the leg statuses
- action: javascript@v1
params:
code: |
$statusA = 'confirmed'
$statusB = 'dialing'
# Call leg B now
- action: dial@v2
params:
callerId: ${{ $cliB }}
answerOnBranch: legB
targets:
- number: ${{ $targetB }}
timeout: 30
result: $dialB
- if: "!$dialB?.ok"
then:
- action: javascript@v1
params:
code: $statusB = $dialB?.status
- action: play@v3
params:
tts:
text: Sorry, we could not reach an agent. Please try again later.
- action: hangup@v1
# The legB branch defines the actions for the second leg of the call (B)
legB:
actions:
- action: javascript@v1
params:
code: |
$count = 0
$statusB = 'answered'
# Ask for confirmation
- label: ask
action: gather@v4
params:
maxDigits: 1
tts:
text: Hello! To accept this call, press 1.
result: $dtmf
- if: $dtmf?.input === '1'
then:
- action: javascript@v1
params:
code: |
$statusA = 'bridged'
$statusB = 'bridged'
- action: bridge@v3
- action: hangup@v1
# Up to 3 attempts
- action: javascript@v1
params:
code: $count++
- if: $count >= 3
action: hangup@v1
- goto:
label: askConference room with PIN
Callers dial in, enter a PIN and wait on hold until someone else joins; your app can call a participant straight into the same room, which they leave automatically when everyone else has gone.
How it works
- Someone calls your number and hears a welcome message.
- They type the PIN code followed by the pound key; after three wrong tries the call ends politely.
- They wait on hold until another participant joins, and everyone in the room hears when someone arrives or leaves.
- When your app starts it, Callr calls a participant and puts them straight into the same room, with no PIN.
- A participant leaves by pressing star or automatically when everyone else has left; the result can go to your webhook.
Starts on: inbound-call, api-initiated
Actions used: conference@v1, play@v3, hangup@v1, dial@v2, fetch@v3
Guide: Conference Calls
# Conference room with a PIN code.
# Callers dial your number, enter the PIN and wait until someone else joins.
# Everyone who enters the same $room is connected together. A caller leaves
# by pressing *, or automatically when everyone else has left.
#
# Your app can also call a participant straight into the room: click "test"
# on the right bar, then "START ACTION" (or "SHOW API CODE" to start it
# with the API, passing $participant).
#
# What to edit: $room, $pin, $callerId (for the API run), the messages,
# optionally $webhookUrl.
# Needs: a Callr number assigned to this scenario; for the API run, a caller ID
# assigned or approved on your account; 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/conference-calls
description: "Conference room with PIN"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Harper:MAI-Voice-2
# Every variable can be overridden for each run started by API
variables:
$conference: null
$dial: null
# MODIFIABLE VARIABLES BELOW
# Room name: 1 to 32 letters or digits, quote it if it is all digits
$room: team
# PIN code to enter the room (1 to 10 digits, quoted)
$pin: "4321"
# API run: number called into the room, pass it in each API call
$participant: "+15550100002"
# API run: caller ID shown to $participant, assigned or approved on your account
$callerId: "+15550100001"
# Receives the conference result, e.g. https://api.example.com/callr/conference
$webhookUrl: null
branches:
# Callers dial in and enter the PIN
inbound-call:
actions:
# The action blocks until the caller leaves the room
- action: conference@v1
params:
room: ${{ $room }}
welcome:
tts:
text: Welcome to the conference.
# Played in a loop while the caller is alone
waiting:
tts:
text: Please hold, the other participants will join shortly.
# Alternative: an audio file of your library instead of text-to-speech
# waiting:
# audioFile:
# sid: "fdsanjkf"
pin:
code: ${{ $pin }}
maxTries: 3
timeout: 10
prompt:
tts:
text: Please enter your PIN code, followed by the pound key.
error:
tts:
text: Wrong PIN code.
valid:
tts:
text: Thank you. You are joining the conference. Press star to leave.
# Played to the whole room: use the same prompts in every branch
userJoin:
tts:
text: A participant has joined.
userLeave:
tts:
text: A participant has left.
autoLeave:
enabled: true
announce:
tts:
text: Everyone else has left the conference. Goodbye.
result: $conference
# Wrong PIN, no PIN or room unavailable: the call is not hung up yet
- if: "!$conference?.ok"
action: play@v3
params:
tts:
text: Sorry, we could not connect you to the conference. Goodbye.
- action: hangup@v1
# Your app starts a run: Callr calls $participant into the room
api-initiated:
actions:
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $participant }}
timeout: 30
answerOnBranch: joinRoom
result: $dial
# $conference is set by the joinRoom branch, on the participant's leg
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"room": ${{ JSON.stringify($room) }},
"participant": ${{ JSON.stringify($participant) }},
"dialStatus": ${{ JSON.stringify($dial?.status ?? null) }},
"status": ${{ JSON.stringify($conference?.status ?? null) }},
"duration": ${{ JSON.stringify($conference?.duration ?? 0) }},
"run": ${{ JSON.stringify(run) }}
}
# Runs for the participant who answers: no PIN needed
joinRoom:
actions:
- action: conference@v1
params:
room: ${{ $room }}
welcome:
tts:
text: You are joining the conference. Press star to leave.
# Same hold message as for callers (see the audio file alternative above)
waiting:
tts:
text: Please hold, the other participants will join shortly.
userJoin:
tts:
text: A participant has joined.
userLeave:
tts:
text: A participant has left.
autoLeave:
enabled: true
announce:
tts:
text: Everyone else has left the conference. Goodbye.
result: $conference
- action: hangup@v1
# Runs when an inbound call ends
hangup:
actions:
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"room": ${{ JSON.stringify($room) }},
"status": ${{ JSON.stringify($conference?.status ?? null) }},
"duration": ${{ JSON.stringify($conference?.duration ?? 0) }},
"call": ${{ JSON.stringify(call) }},
"run": ${{ JSON.stringify(run) }}
}Connect buyers and sellers
A pool number per listing: your platform says who to connect, Callr calls them showing the masked number, whispers the context, can record for disputes and posts the CDR back to your platform.
How it works
- Someone calls the masked number shown on a listing.
- Callr asks your platform who to connect for this number and this caller; if no session is active, the caller is told the number is no longer in service.
- Callr calls the other party, showing the masked number, so neither side sees the other's real number.
- The person called first hears a short message about the listing, then the two are connected; the call can be recorded for disputes.
- When the call ends, the outcome and the full call record are posted to your platform.
Starts on: inbound-call
Actions used: play@v3, hangup@v1, fetch@v3, startRecording@v2, dial@v2, bridge@v3, sms@v1
Guide: Marketplace Call Masking
# Marketplace call masking: buyers and sellers talk without seeing
# each other's real number. Each listing (or each buyer/seller pair)
# gets a Callr number from your pool. When someone calls it, your
# platform says who to connect, Callr calls that person showing the
# masked number as caller ID, whispers the context, records the call
# if you want, and posts the outcome and the full CDR back to you.
# The same scenario works in both directions.
#
# What to edit: $lookupUrl and $eventsUrl (your platform), $brand,
# then the optional $whisper, $rejectAnonymous and $recordForDisputes.
# Needs: an API on your platform. $lookupUrl receives
# { maskedNumber, from } and answers { "target": "+1555...", "whisper": "..." }
# ("whisper" is optional), or an error / no target when no session
# is active. Attach every pool number to this scenario.
# Your platform must accept requests from the Callr IP ranges:
# https://docs.callr.com/docs/ip-subnets
# Build Mode: calls only go to your Testing Numbers.
# Guide: https://docs.callr.com/docs/marketplace-call-masking
description: "Connect buyers and sellers"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Harper:MAI-Voice-2
variables:
$lookup: null
$dial: null
# MODIFIABLE VARIABLES BELOW
$lookupUrl: https://api.example.com/masking/lookup # who to connect for this masked number and caller
$eventsUrl: https://api.example.com/masking/events # receives the call outcome, the CDR and recordings
$brand: Example Homes # your marketplace name, said to callers when no session is active
$whisper: You have a call about your listing. # heard by the person called, unless the lookup returns "whisper"
$rejectAnonymous: false # true to refuse callers who hide their number
$recordForDisputes: false # true to announce and record every connected call
branches:
inbound-call:
actions:
# Optional: refuse hidden numbers, your platform cannot match them
- if: $rejectAnonymous && call.cdr?.callerRestricted
then:
- action: play@v3
params:
tts:
text: Please call again without hiding your number.
- action: hangup@v1
# Ask your platform who to connect for this masked number and caller
- action: fetch@v3
params:
url: ${{ $lookupUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"maskedNumber": ${{ JSON.stringify(call.toNumber) }},
"from": ${{ JSON.stringify(call.fromNumber) }}
}
responseFormat: json
timeout: 5
result: $lookup
# No active session for this pair: say so and hang up.
# The parsed response is in $lookup.json ($lookup.body is the raw text).
- if: "!$lookup?.ok || !$lookup?.json?.target"
then:
- action: play@v3
params:
tts:
text: This number is no longer in service. Please contact the seller through ${{ $brand }}.
- action: hangup@v1
# Optional: record for disputes, announced before it starts.
# A recording is also sent when the dial fails; match it to its
# call event with "callId".
- if: $recordForDisputes
then:
- action: play@v3
params:
tts:
text: This call may be recorded.
- action: startRecording@v2
params:
transcription:
enabled: false
events:
# Runs when the recording file is ready
ready:
- action: fetch@v3
params:
url: ${{ $eventsUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"type": "recording",
"callId": ${{ JSON.stringify(call.id) }},
"maskedNumber": ${{ JSON.stringify(call.toNumber) }},
"from": ${{ JSON.stringify(call.fromNumber) }},
"recordingUrl": ${{ JSON.stringify(recording?.url) }},
"duration": ${{ JSON.stringify(recording?.duration) }}
}
- action: play@v3
params:
tts:
text: Connecting you, please hold.
# Call the other party, showing the masked number as caller ID
# (allowed: the pool number is a Callr number attached to this scenario)
- action: dial@v2
params:
callerId: ${{ call.toNumber }}
targets:
- number: ${{ $lookup.json.target }}
timeout: 30
answerOnBranch: whisper
result: $dial
- if: "!$dial?.ok"
action: play@v3
params:
tts:
text: The person you are trying to reach is not available. Please try again later.
- action: hangup@v1
# Heard by the person called only, before the two sides are connected
whisper:
actions:
- action: play@v3
params:
tts:
text: ${{ $lookup?.json?.whisper || $whisper }}
- if: $recordForDisputes
action: play@v3
params:
tts:
text: This call may be recorded.
- action: bridge@v3
- action: hangup@v1
# Runs once the call has ended: report it to your platform
hangup:
actions:
- action: fetch@v3
params:
url: ${{ $eventsUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"type": "call",
"callId": ${{ JSON.stringify(call.id) }},
"maskedNumber": ${{ JSON.stringify(call.toNumber) }},
"from": ${{ JSON.stringify(call.fromNumber) }},
"target": ${{ JSON.stringify($lookup?.json?.target ?? null) }},
"connected": ${{ JSON.stringify(!!$dial?.ok) }},
"status": ${{ JSON.stringify($dial?.status ?? null) }},
"cdr": ${{ JSON.stringify(call.cdr) }}
}
# Alternative: relay texts too. A text sent to the masked number is
# forwarded to the other party, from the masked number.
# inbound-sms:
# actions:
# - action: fetch@v3
# params:
# url: ${{ $lookupUrl }}
# method: POST
# headers:
# Content-Type: application/json
# body: |
# {
# "maskedNumber": ${{ JSON.stringify(sms.to) }},
# "from": ${{ JSON.stringify(sms.from) }}
# }
# responseFormat: json
# timeout: 5
# result: $lookup
# - if: $lookup?.ok && $lookup?.json?.target
# action: sms@v1
# params:
# to:
# - ${{ $lookup.json.target }}
# from: ${{ sms.to }}
# body: ${{ sms.body?.text ?? '' }}
# nature: ALERTINGUpdated 40 minutes ago
