Reminders & alerts
Outbound calls started from the API: reminders, one-time codes and alert escalation.
Appointment reminder
Started from your app by API, it calls the customer, skips answering machines, names your business, reads the appointment, takes press 1 to confirm or 2 to cancel, and posts the answer to your webhook.
How it works
- Your app starts a call to the customer with the appointment details.
- If an answering machine picks up, the call ends without a message.
- The customer hears who is calling and the reminder, then presses 1 to confirm or 2 to cancel.
- The answer, or the reason the call failed, is sent to your server.
Starts on: api-initiated
Actions used: dial@v2, javascript@v1, fetch@v3, amd@v3, hangup@v1, play@v3, gather@v4
Guide: Broadcast Calls
# 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".
#
# Appointment reminder: calls the customer, skips answering machines,
# reads the appointment and asks to press 1 to confirm or 2 to cancel,
# then posts the answer to your server. Start one run per customer.
#
# What to edit: $target, $callerId, $appointment and $url (pass
# them per call with the API), $company and the prompts.
# Leave $url empty (null) to skip the push.
#
# Needs: a caller ID: a Callr number attached to an active scenario
# or an approved caller ID. In Build Mode, calls only reach your
# Testing Numbers. In France, a caller ID is blocked above 250 calls
# a day (use NPV numbers), and marketing calls are only allowed on
# weekdays, 10:00-13:00 and 14:00-20:00.
# https://docs.callr.com/docs/caller-ids
#
# If your server filters by IP address, allow the Callr subnets:
# https://docs.callr.com/docs/ip-subnets
#
# Guide: https://docs.callr.com/docs/broadcast-calls
description: "Appointment reminder"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Olivia:MAI-Voice-2
variables:
$dial: null
$amd: null
$dtmf: null
# the customer's answer, pushed to your server
$result: "unknown"
# MODIFIABLE VARIABLES BELOW
# your own ID for this call, sent back with the result
$userID: 0
# the number to call
$target: "+33639980000"
# the number shown to the customer: a Callr number or an approved caller ID
$callerId: "+33199001234"
# your business name, read at the start of the call
$company: Heart Care
# the appointment, read to the customer
$appointment: today at 5 PM
# how long the phone rings, in seconds
$timeout: 30
# how many times the reminder is read before hanging up
$repeat: 3
# receives the result (POST, JSON), e.g. https://api.example.com/callr/result
$url: null
branches:
# Runs when the API starts the scenario.
api-initiated:
actions:
# Call the customer. This action waits until the call
# ends or rings out; the answered call runs 'answered'.
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $target }}
timeout: ${{ $timeout }}
answerOnBranch: answered
result: $dial
# Not answered: keep the reason (busy, noanswer, failed...)
- if: "$dial?.status !== 'answered'"
action: javascript@v1
params:
code: $result = $dial?.status ?? 'failed'
# Push the result for this call
- if: $url
action: fetch@v3
params:
url: ${{ $url }}?userID=${{ encodeURIComponent($userID) }}
method: POST
headers:
Content-Type: application/json
body: |
{
"target": ${{ JSON.stringify($target) }},
"result": ${{ JSON.stringify($result) }},
"dial": ${{ JSON.stringify($dial) }}
}
# Runs on the customer's call once answered.
answered:
actions:
# Skip answering machines
- action: amd@v3
params:
timeout: 4
result: $amd
- if: $amd?.detected == 'machine'
then:
- action: javascript@v1
params:
code: $result = 'voicemail'
- action: hangup@v1
- label: broadcast
action: play@v3
params:
tts:
text: Hello! This is ${{ $company }}. This is a reminder of your appointment ${{ $appointment }}.
- action: javascript@v1
params:
code: $repeat--
# Ask for confirmation
- action: gather@v4
params:
maxDigits: 1
timeout: 10
tts:
text: Press 1 to confirm, or 2 to cancel.
result: $dtmf
- if: $dtmf?.input === '1'
then:
- action: javascript@v1
params:
code: $result = 'confirmed'
- action: play@v3
params:
tts:
text: Thank you! Your appointment is confirmed.
- action: hangup@v1
- if: $dtmf?.input === '2'
then:
- action: javascript@v1
params:
code: $result = 'canceled'
- action: play@v3
params:
tts:
text: OK. Your appointment is canceled.
- action: hangup@v1
# Alternative: a campaign message, press 1 to talk to sales,
# 9 to opt out. Replace the gather and the two checks above with:
# - action: gather@v4
# params:
# maxDigits: 1
# timeout: 10
# tts:
# text: Press 1 to talk to our sales team, or 9 to stop receiving our calls.
# result: $dtmf
#
# - if: $dtmf?.input === '1'
# then:
# - action: javascript@v1
# params:
# code: $result = 'sales'
# - action: dial@v2
# params:
# callerId: ${{ $callerId }}
# targets:
# - number: "+33199004210"
# timeout: 30
# - action: hangup@v1
#
# - if: $dtmf?.input === '9'
# then:
# - action: javascript@v1
# params:
# code: $result = 'opt-out'
# - action: play@v3
# params:
# tts:
# text: Done. You will not receive our calls anymore. Goodbye.
# - action: hangup@v1
# Neither 1 nor 2 (or no key pressed)
- action: play@v3
params:
tts:
text: Sorry, I did not get that.
- if: $repeat <= 0
action: hangup@v1
- goto:
label: broadcastVoice OTP with SMS fallback
Your app calls the user and the code is read out digit by digit; if nobody answers, it goes by SMS and your webhook gets the result.
How it works
- Your app starts the scenario with the phone number and the code to deliver.
- Callr calls the user from your caller ID and reads the code digit by digit, twice.
- The user can press 1 to hear the code again, a limited number of times.
- If the call is not answered, the code is sent by SMS instead.
- The delivery result (voice, SMS or none) is posted to your webhook.
Starts on: api-initiated
Actions used: dial@v2, javascript@v1, sms@v1, fetch@v3, play@v3, gather@v4, hangup@v1
Guide: Start a Callr Actions flow with the API
# 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".
#
# Voice one-time password: your app starts a run with $target and $code,
# Callr calls the user and reads the code digit by digit, twice.
# The user can press 1 to hear it again. If the call is not answered,
# the code is sent by SMS. The delivery result is posted to your webhook.
#
# What to edit: $callerId, then pass $target and $code in each API call;
# optionally $webhookUrl and the messages.
# Needs: a caller ID assigned or approved on your account, SMS credit for the
# fallback, a webhook (optional).
# Limit: a voicemail pickup counts as answered (status "answered"):
# the code is left in the voicemail box and no SMS follows.
# Build Mode: calls and SMS only to your Testing Numbers.
# Webhook source IPs: https://docs.callr.com/docs/ip-subnets
# Docs: https://docs.callr.com/recipes/start-callr-actions-flow-with-the-api
description: "Voice OTP with SMS fallback"
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:
$dial: null
$spoken: null
$key: null
$repeats: 0
$deliveredBy: "none"
# MODIFIABLE VARIABLES BELOW
# Number to call, pass it in each API call
$target: "+15550100002"
# Code to read, as a quoted string to keep leading zeros
$code: "482913"
# Caller ID shown to the user: a number assigned or approved on your account
$callerId: "+15550100001"
# How many times the user can press 1 to hear the code again
$maxRepeats: 2
# Send the code by SMS when the call is not answered
$smsFallback: true
# Receives the delivery result, e.g. https://api.example.com/callr/otp
$webhookUrl: null
branches:
api-initiated:
actions:
# Alternative: SMS only, no call. Delete the dial@v2 action below
# and keep $smsFallback true: the fallback then sends the code by SMS
# and the webhook reports deliveredBy "sms".
# Call the user; blocks until the call ends
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $target }}
timeout: 25
answerOnBranch: readCode
result: $dial
# dial@v2 ok is true only when bridged: this flow never bridges, test the status.
# An answering machine also counts as answered.
- if: "['answered', 'bridged'].includes($dial?.status)"
action: javascript@v1
params:
code: $deliveredBy = 'voice'
# Not answered: send the code by SMS
- if: $deliveredBy === 'none' && $smsFallback
then:
- action: sms@v1
params:
to:
- ${{ $target }}
body: Your verification code is ${{ $code }}
nature: ALERTING
# Approved sender ID or one of your Callr numbers
# from: MyCompany
- action: javascript@v1
params:
code: $deliveredBy = 'sms'
# Post the delivery result to your server
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"target": ${{ JSON.stringify($target) }},
"delivered": ${{ JSON.stringify($deliveredBy !== 'none') }},
"deliveredBy": ${{ JSON.stringify($deliveredBy) }},
"status": ${{ JSON.stringify($dial?.status ?? null) }}
}
# Runs on the user's phone once the call is answered
readCode:
actions:
# "482913" becomes "4, 8, 2, 9, 1, 3": a short pause between digits
- action: javascript@v1
params:
code: return String($code).split('').join(', ')
result: $spoken
- label: say
action: play@v3
params:
tts:
text: Your verification code is ${{ $spoken }}. Again, ${{ $spoken }}.
- action: gather@v4
params:
maxDigits: 1
timeout: 6
tts:
text: To hear it again, press 1.
result: $key
# Repeat, up to $maxRepeats times
- if: $key?.input === '1' && $repeats < $maxRepeats
then:
- action: javascript@v1
params:
code: $repeats++
- goto:
label: say
- action: play@v3
params:
tts:
text: Goodbye.
- action: hangup@v1Critical alert escalation
Started by API with the alert text: on-call contacts are called one after another until one acknowledges by pressing 1; up to 10 contacts get an SMS copy and your webhook learns who acknowledged, or that nobody did.
How it works
- Your app starts the scenario with the alert text.
- Callr calls the first on-call contact and reads the alert.
- The contact presses 1 to acknowledge; if nobody answers or presses 1, Callr calls the next contact.
- Calls stop at the first acknowledgment or after the last contact.
- Contacts get an SMS copy with the outcome (up to 10, can be turned off), and your webhook receives who acknowledged and the status of each call.
Starts on: api-initiated
Actions used: javascript@v1, dial@v2, sms@v1, fetch@v3, play@v3, gather@v4, hangup@v1
Guide: Broadcast Calls
# 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".
#
# Critical alert escalation: your app starts a run with the alert text.
# Callr calls the on-call contacts one after another. Each contact hears
# the alert and presses 1 to acknowledge; the calls stop at the first
# acknowledgment. Then every contact gets an SMS copy, and your webhook
# gets who acknowledged, or that nobody did, with the status of each call.
#
# What to edit: $contacts (in escalation order), $callerId, then pass
# $message in each API call; optionally $smsCopy and $webhookUrl.
# Needs: a caller ID assigned or approved on your account, SMS credit,
# a webhook (optional).
# Build Mode: calls and SMS only to your Testing Numbers.
# Test with the live debugger: $acked is set on the contact's call and
# read back here after dial@v2 returns.
# Webhook source IPs: https://docs.callr.com/docs/ip-subnets
# Docs: https://docs.callr.com/docs/broadcast-calls
description: "Critical alert escalation"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Ethan:MAI-Voice-2
# Every variable can be overridden for each run started by API
variables:
$dial: null
$key: null
$current: null
$index: 0
$tries: 0
$acked: false
$ackedBy: null
# One entry per call: number and dial status, posted to your webhook
$attempts: []
# MODIFIABLE VARIABLES BELOW
# On-call contacts, called in this order (up to 10 for the SMS copy)
$contacts:
- "+33639980001"
- "+33639980002"
- "+33639980003"
# The alert, read to each contact; pass it in each API call
# (keep it short: one or two sentences, it is also sent by SMS)
$message: The production database is not responding.
# Caller ID shown to the contacts: a number assigned or approved on your account
$callerId: "+33199001234"
# How long each phone rings, in seconds
$timeout: 25
# Send an SMS copy of the alert and its outcome to every contact
$smsCopy: true
# Receives the outcome, e.g. https://api.example.com/callr/alert
$webhookUrl: null
branches:
api-initiated:
actions:
# Next contact in the list, or null when everyone was called
- label: next
action: javascript@v1
params:
code: $current = $contacts[$index] ?? null
# Call this contact; blocks until the call ends. The loop stops
# at the first acknowledgment or after the last contact.
- if: $current && !$acked
then:
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $current }}
timeout: ${{ $timeout }}
answerOnBranch: announce
result: $dial
- action: javascript@v1
params:
code: |
$attempts.push({ number: $current, status: $dial?.status ?? 'failed' })
$index++
- goto:
label: next
# SMS copy to every contact, acknowledged or not
- if: $smsCopy
action: sms@v1
params:
to: ${{ $contacts }}
body: "ALERT: ${{ $message }} ${{ $acked ? 'Acknowledged by ' + $ackedBy + '.' : 'Not acknowledged.' }}"
nature: ALERTING
# Post the outcome to your server
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"message": ${{ JSON.stringify($message) }},
"acked": ${{ JSON.stringify($acked) }},
"ackedBy": ${{ JSON.stringify($ackedBy) }},
"attempts": ${{ JSON.stringify($attempts) }}
}
# Runs on the contact's phone once the call is answered
announce:
actions:
# A new contact: reset the counter
- action: javascript@v1
params:
code: $tries = 0
# Read the alert first, then ask for the key: the gather timeout also
# covers its prompt, so it stays short whatever the alert length.
# An answering machine never presses 1, so it never acknowledges.
- label: alert
action: play@v3
params:
tts:
text: This is a critical alert. ${{ $message }}
- action: gather@v4
params:
maxDigits: 1
timeout: 8
tts:
text: Press 1 to acknowledge.
result: $key
- if: $key?.input === '1'
then:
- action: javascript@v1
params:
code: |
$acked = true
$ackedBy = $current
- action: play@v3
params:
tts:
text: Thank you. The alert is acknowledged. Goodbye.
- action: hangup@v1
# Read the alert twice at most, then move on to the next contact
- action: javascript@v1
params:
code: $tries++
- if: $tries >= 2
then:
- action: play@v3
params:
tts:
text: No acknowledgment. We will call the next contact. Goodbye.
- action: hangup@v1
- goto:
label: alertUpdated about 3 hours ago
