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

  1. When someone calls your number, the call entry point runs.
  2. When someone texts your number, the SMS entry point runs.
  3. When your app starts the scenario by API, the API entry point runs.
  4. 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 API

Your 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

  1. Someone calls your number and hears a short welcome message.
  2. The call ends on its own after a second sentence.
  3. 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.
  4. 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

  1. Someone calls your number and hears it ringing.
  2. Meanwhile, your API is asked who is calling, with the caller's number.
  3. The call is answered and the caller is greeted with what your API returned.
  4. 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

  1. While the phone rings, the caller's number is searched in your Salesforce Contacts and Leads.
  2. The call is answered and the caller is greeted by name when they are found.
  3. The call goes to your team, or to the record owner if you mapped owners to numbers.
  4. Before being connected, your rep hears who is calling and whether it is a contact or a lead.
  5. 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: 5

Your 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

  1. Someone calls your number.
  2. Every message of the scenario is spoken with your ElevenLabs voice.
  3. The caller hears a welcome message.
  4. 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@v1

Did this page help you?