Marketplace Call Masking

Connect buyers and sellers through temporary Callr numbers that keep their real numbers private, with your platform deciding who talks to whom.

👍

A buyer calls the number shown on a listing; the seller's phone rings; neither sees the other's real number. Your platform stays in the loop: it decides the routing, tracks every call, and can cut the link when the deal is done.

⚙️ How It Works

  1. Your platform owns a pool of Callr numbers (bought with the Phone Numbers API or in the console), all attached to the same scenario.
  2. When a buyer contacts a seller, your platform assigns a masked number from the pool to the pair (buyer, seller) for a limited time, and shows it to the buyer.
  3. The buyer calls the masked number. The scenario asks your platform who is on the other side with fetch@v3, sending the number called (call.toNumber) and the caller (call.fromNumber).
  4. dial@v2 connects the call to the seller, with the masked number as caller ID: the seller sees the Callr number, not the buyer's.
  5. In the hangup branch, the call details are pushed back to your platform.
📘

The same scenario works in both directions. When the seller calls the masked number back, your platform recognizes the pair from (toNumber, fromNumber) and returns the buyer's number.


🚀 The Scenario

description: Marketplace call masking

compat:
  version: "2025-09-01"

defaults:
  language: en-US

variables:
  $lookupUrl: https://api.example.com/masking/lookup
  $eventsUrl: https://api.example.com/masking/events
  $lookup: null
  $dial: null

branches:
  inbound-call:
    actions:
      # 1. Ask the platform who to connect
      - 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

      # 2. No active session for this pair: say so and hang up
      - if: '!$lookup.ok || !$lookup.body.target'
        then:
          - action: play@v3
            params:
              tts:
                text: This number is no longer in service. Please contact the seller through the application.
          - action: hangup@v1

      # 3. Connect, showing the masked number as caller ID
      - action: play@v3
        params:
          tts:
            text: Connecting you, please hold.

      - action: dial@v2
        params:
          callerId: ${{ call.toNumber }}
          targets:
            - number: ${{ $lookup.body.target }}
              timeout: 30
        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

  hangup:
    actions:
      # 4. Report the call to the platform
      - action: fetch@v3
        params:
          url: ${{ $eventsUrl }}
          method: POST
          headers:
            content-type: application/json
          body: |
            {
              "maskedNumber": ${{ JSON.stringify(call.toNumber) }},
              "from": ${{ JSON.stringify(call.fromNumber) }},
              "target": ${{ JSON.stringify($lookup && $lookup.body && $lookup.body.target) }},
              "connected": ${{ JSON.stringify(!!($dial && $dial.ok)) }},
              "status": ${{ JSON.stringify($dial && $dial.status) }},
              "cdr": ${{ JSON.stringify(call.cdr) }}
            }

What Your Platform Must Answer

The lookup endpoint receives the masked number and the caller, and answers with the real number to call — or nothing when the session has expired:

{ "target": "+33639980042" }
🚧

Anonymous callers. call.fromNumber is empty when the caller hides their number (call.callerRestricted is true). Decide what to do with them: refuse the call, or route by masked number only when a single session uses it.


🔢 Managing the Number Pool

A masked number is reused: once a session expires, it can be assigned to another pair. The rules live in your platform, not in the scenario:

  • One number, one active pair. Keep sessions short (a few days after the last call) so the pool stays small.
  • Match on both numbers. A seller may be in several sessions at once; (maskedNumber, from) identifies the right one.
  • Choose numbers close to the users. Buy numbers in the country of the listing with the Phone Numbers API: a local number gets answered more often, and some countries restrict the caller IDs you can present (see Caller IDs).
  • Attach every pool number to the scenario with Attach number to scenario when you buy it.

🎙️ Optional: Record for Disputes

Marketplaces often record masked calls to settle disputes. Announce it, then add startRecording@v1 before the dial and push the recording URL when it is ready:

- action: play@v3
  params:
    tts:
      text: This call may be recorded.

- action: startRecording@v1
  params:
    transcription:
      enabled: false
  events:
    ready:
      - action: fetch@v3
        params:
          url: https://api.example.com/masking/recordings
          method: POST
          headers:
            content-type: application/json
          body: |
            {
              "maskedNumber": ${{ JSON.stringify(call.toNumber) }},
              "from": ${{ JSON.stringify(call.fromNumber) }},
              "recordingUrl": ${{ JSON.stringify(recording.url) }}
            }

See Record and Transcribe Calls for the details.


💡 Going Further

  • Click-to-call from the app: instead of showing a masked number, start the call from your platform with two outbound legs: Connect Two Numbers.
  • SMS masking: attach the pool numbers to an inbound-sms branch and relay messages with sms@v1, using the same lookup. See Receive SMS.
  • Qualify the call: ask the buyer to press a key before ringing the seller (gather@v4), to filter robocalls. See Build an IVR Menu.
  • Measure it: every masked call ends in the hangup branch with a full CDR: see Call Detail Records.

Did this page help you?