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
- Your platform owns a pool of Callr numbers (bought with the Phone Numbers API or in the console), all attached to the same scenario.
- 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.
- 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). dial@v2connects the call to the seller, with the masked number as caller ID: the seller sees the Callr number, not the buyer's.- In the
hangupbranch, 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.fromNumberis empty when the caller hides their number (call.callerRestrictedistrue). 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-smsbranch and relay messages withsms@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
hangupbranch with a full CDR: see Call Detail Records.
Updated about 2 hours ago
Did this page help you?
