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

  1. Your app starts a call to the customer with the appointment details.
  2. If an answering machine picks up, the call ends without a message.
  3. The customer hears who is calling and the reminder, then presses 1 to confirm or 2 to cancel.
  4. 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: broadcast

Voice 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

  1. Your app starts the scenario with the phone number and the code to deliver.
  2. Callr calls the user from your caller ID and reads the code digit by digit, twice.
  3. The user can press 1 to hear the code again, a limited number of times.
  4. If the call is not answered, the code is sent by SMS instead.
  5. 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@v1

Critical 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

  1. Your app starts the scenario with the alert text.
  2. Callr calls the first on-call contact and reads the alert.
  3. The contact presses 1 to acknowledge; if nobody answers or presses 1, Callr calls the next contact.
  4. Calls stop at the first acknowledgment or after the last contact.
  5. 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: alert

Did this page help you?