Introduction
Imagine a delivery management system where couriers can track their assigned deliveries, check customer addresses, and update delivery statuses.
During a delivery, situations may arise where the courier needs to contact the customer. Perhaps the address is difficult to find, the building entrance is locked, or nobody is answering the door.
A common solution is to add a Call Customer button to the delivery screen. When the courier clicks it, the application retrieves the customer’s phone number and opens the device dialer.
From a functional perspective, this solves the problem. The courier can quickly contact the customer and continue the delivery.
However, this approach introduces a privacy concern that might not be immediately obvious.
By exposing the customer’s phone number, the platform gives the courier access to personal information that can be stored or used outside the context of the delivery. The opposite can also happen: when the customer receives the call, they may see the courier’s personal phone number.
What was supposed to be a temporary interaction between two people becomes an opportunity for unwanted contact after the delivery is completed.
This is the problem that Masked Calling aims to solve.
What is Masked Calling?
Masked Calling, also known as Number Masking, is a communication mechanism that allows two people to make phone calls without revealing their real phone numbers to each other.
Instead of connecting the courier directly to the customer’s phone number, the system introduces an intermediary number managed by a telephony provider.
Imagine that a courier needs to contact a customer during a delivery. The platform already knows both phone numbers, but neither participant needs to know the other’s number.
The platform can temporarily associate a virtual phone number with their communication. When the courier calls that number, the telephony provider identifies the caller, determines the intended recipient based on the active communication session, and forwards the call.
The customer receives the call normally, but the caller ID displays an intermediary number rather than the courier’s personal number, assuming the provider is configured to present the appropriate caller ID.
The same mechanism can work in the opposite direction, allowing the customer to return the call without discovering the courier’s number.
The key idea is that the platform enables communication without sharing personal contact information between participants.
How it works in a delivery system
Let’s consider a delivery identified as delivery_123, assigned to a courier and associated with a customer.
The platform already stores the customer’s phone number because it may be necessary for delivery operations. However, this information does not need to be included in the API response sent to the courier’s application.
In a traditional implementation, the delivery endpoint might return something like:
{
"id": "delivery_123",
"customer": {
"name": "John",
"phone": "+15551111111"
}
}
The frontend then uses the phone property to initiate a call.
With Masked Calling, we can change this responsibility. Instead of exposing the phone number, the backend can inform the application whether the courier is allowed to contact the customer.
{
"id": "delivery_123",
"customer": {
"name": "John",
"canCall": true
}
}
The canCall property is useful for controlling the interface, but it should not replace authorization checks in the backend.
When the courier clicks Call Customer, the application requests a communication session associated with that specific delivery.
A possible endpoint would be:
POST /deliveries/delivery_123/call
At this point, the backend verifies whether the authenticated courier is assigned to the delivery, whether the delivery allows communication, and whether an existing session can be reused.
After these validations, the system must determine how the actual phone call will be established.
There are two common approaches.
Dial-to-Proxy
In this approach, the backend associates a temporary virtual phone number with the delivery and returns that number to the application.
The frontend can then open the device dialer using the virtual number instead of the customer’s real phone number.
When the courier places the call, the telephony provider receives it and uses the caller’s number and the dialed virtual number to identify the appropriate communication session.
The provider then forwards the call to the customer, using an appropriate masked caller ID.
This approach works well with regular phone dialers because the courier initiates the call in a familiar way.
However, the backend must ensure that the combination of caller and virtual number identifies exactly one permitted destination.
Click-to-Call
Another approach is to let the backend initiate the communication through the provider.
When the courier clicks Call Customer, the application sends the delivery identifier to the backend. After authorization, the backend asks the provider to initiate the call, often by calling the courier first and then connecting the customer.
Because the backend already knows the requested delivery, it does not need to infer the destination from an incoming call to a shared virtual number.
This can simplify routing decisions, but it introduces a different user experience: the courier may receive an incoming call rather than seeing the device dialer open.
The provider also needs to support the required outbound call orchestration and caller ID configuration.
Both approaches can provide number masking, but they involve different responsibilities. Returning a virtual number to the frontend is not the same as asking a telephony provider to initiate a call.
The implementation should choose one of these models explicitly rather than treating them as interchangeable.
Managing communication sessions
A useful architectural decision is to represent masked communication as a temporary session associated with a specific delivery.
For example:
interface MaskedCallSession {
id: string;
deliveryId: string;
courierId: string;
customerId: string;
providerSessionId: string | null;
providerNumber: string | null;
status: "ACTIVE" | "EXPIRED" | "TERMINATED";
expiresAt: Date;
createdAt: Date;
}
This session represents the temporary relationship between the courier and customer. It defines who is allowed to communicate, under which delivery, and for how long.
However, an important distinction must be made between a communication session and an individual phone call.
A communication session may allow multiple calls during the same delivery. Each call has its own lifecycle, potentially including states such as INITIATED, RINGING, IN_PROGRESS, COMPLETED, or FAILED.
For example, a courier might attempt to contact the customer twice before receiving an answer. Both attempts can belong to the same communication session, but they are separate calls.
This distinction becomes especially important when the delivery is completed.
Suppose the courier starts a call at 2:43 PM, and the delivery is marked as completed at 2:45 PM while the conversation is still in progress.
Invalidating the communication session should prevent new calls from being initiated or routed, but it does not necessarily disconnect an already established call.
Ending an active call requires an explicit operation supported by the telephony provider.
The platform therefore needs to define whether an ongoing conversation should be allowed to finish or be actively terminated when the delivery ends.
For most delivery scenarios, allowing an already established call to finish while preventing new calls may provide a better user experience. However, stricter policies may be necessary depending on the application’s security and operational requirements.
Defining when communication expires
The delivery status is a useful starting point for determining when communication should be available, but it should not be the only rule.
For example, a platform might allow calls while a delivery is ASSIGNED, PICKED_UP, or IN_TRANSIT, then provide a short grace period after completion to resolve final delivery questions.
Other events may require immediate changes to communication permissions. If a courier is reassigned, the previous courier should lose access to the customer, even if the delivery remains active.
A robust implementation should consider delivery completion, cancellation, reassignment, session expiration, ongoing calls, and any post-delivery communication policy.
The central idea is that the communication session has its own lifecycle, governed by delivery-related business rules but not necessarily identical to the delivery lifecycle.
Architectural challenges
Although the concept behind Masked Calling is relatively straightforward, implementing it introduces challenges involving call routing, telephony infrastructure, authorization, and asynchronous state management.
Routing calls to the correct customer
One of the main challenges is ensuring that each incoming call reaches the correct recipient.
Imagine that a courier has three active deliveries, each associated with a different customer.
If the courier uses the same virtual number for all three deliveries, the provider may receive a call without enough information to determine which customer should receive it.
For example, suppose the provider sees:
Caller: courier_789
Dialed number: virtual_number_001
If this combination matches three active communication sessions, the destination becomes ambiguous.
The provider cannot determine which delivery the courier intended to contact based only on those two phone numbers.
One solution is to allocate different virtual numbers to sessions involving the same courier, ensuring that the caller and dialed number identify a unique destination.
Another solution is to use Click-to-Call, where the application explicitly identifies the delivery before the provider initiates the call.
A number pool can also be shared across different communication sessions, provided the routing rules prevent ambiguous mappings. For example, the same virtual number might serve unrelated couriers whose source numbers identify distinct sessions.
The goal is not necessarily to allocate a unique phone number to every delivery, but to guarantee that every incoming call can be matched to exactly one authorized communication session.
Managing virtual phone numbers
Virtual phone numbers are limited resources and usually involve financial costs.
For a platform processing thousands of deliveries every day, permanently assigning a number to every delivery would be inefficient.
A more practical approach is to maintain a pool of virtual numbers that can be temporarily allocated and reused.
However, reusing numbers requires careful coordination.
Suppose a customer calls a previously assigned virtual number after their delivery has been completed. If that number has already been allocated to another communication session, the system must ensure that the old call does not reach an unrelated person.
This means number allocation needs to consider active sessions, participant identifiers, routing conflicts, and the possibility of delayed calls.
Depending on the provider, some of these responsibilities may be managed automatically, while others must be implemented by the application.
Authorization and abuse prevention
Before allowing a call, the backend should verify that the authenticated courier is assigned to the requested delivery and that communication is permitted.
These checks should also apply when the provider asks the backend to route an incoming call. Otherwise, a previously valid virtual number might continue forwarding calls after the associated permission has expired.
The platform should also protect against abusive behavior.
A courier should not be able to initiate unlimited calls to the same customer. Rate limiting, cooldown periods, and restrictions based on delivery status can reduce harassment risks and unnecessary telephony costs.
In the Dial-to-Proxy model, these protections must account for calls initiated directly through the phone network, not only requests originating from the application.
Webhooks and asynchronous events
Telephony providers commonly report call activity through webhooks.
These notifications allow the backend to track events such as call initiation, ringing, answering, completion, and failure.
However, webhook-based integrations introduce familiar distributed systems challenges.
Events may arrive more than once, be delayed, or arrive in a different order than expected. The application should process them idempotently and avoid making incorrect state transitions based on duplicate or outdated information.
For example, an older RINGING event should not incorrectly move a call back from COMPLETED to RINGING.
The backend must also verify that incoming webhook requests originate from the configured provider, following its supported signature validation mechanism.
Another important consideration is failure isolation. A call may fail because the customer is unavailable, the provider experiences an outage, or the number allocation process fails.
These failures should be handled as communication failures rather than automatically changing the delivery status.
Separating business rules from the provider
Integrating a telephony provider directly into delivery use cases can create unnecessary coupling.
A better approach is to separate application-level communication rules from provider-specific operations.
For example, an application service could be responsible for validating the delivery, determining the communication policy, and deciding when sessions should become unavailable.
A dedicated infrastructure adapter would then translate those operations into the API calls required by the provider.
An abstraction could expose capabilities such as:
interface MaskedCallingProvider {
createSession(input: CreateCallSessionInput): Promise<ProviderCallSession>;
initiateCall(input: InitiateCallInput): Promise<ProviderCall>;
disableRouting(sessionId: string): Promise<void>;
terminateCall(callId: string): Promise<void>;
}
This interface is illustrative rather than a universal contract. The exact operations depend on whether the system uses Dial-to-Proxy, Click-to-Call, or a combination of both.
The important detail is that disabling a communication session and terminating an active call are modeled as different operations.
A production implementation would also need to handle provider identifiers, status callbacks, partial failures, and synchronization between local and remote state.
This separation makes the application easier to test and allows telephony providers to be replaced without rewriting the delivery domain.
Observability and auditing
Although Masked Calling reduces the exposure of personal phone numbers, those numbers still exist within the backend and telephony infrastructure.
Developers must therefore be careful about how sensitive information appears in logs, monitoring tools, and audit records.
Instead of recording real source and destination phone numbers, the platform can use internal identifiers associated with the delivery and communication session.
{
"event": "delivery.masked_call.started",
"deliveryId": "delivery_123",
"courierId": "courier_789",
"customerId": "customer_456",
"callSessionId": "call_session_001"
}
These identifiers provide enough context to investigate operational issues without unnecessarily duplicating personal information across multiple systems.
Provider call identifiers, timestamps, call duration, and final status can also be useful for troubleshooting, provided they are stored with appropriate access controls and retention policies.
Privacy by Design
The main benefit of Masked Calling is not simply replacing one phone number with another.
It represents a different way of designing communication features.
In a conventional implementation, the frontend receives the customer’s phone number because that is the easiest way to initiate a call. However, this also means personal information may be exposed through API responses, browser state, application logs, analytics tools, and other parts of the system.
With Masked Calling, the platform can minimize this exposure by keeping phone numbers within a restricted set of backend components and the telephony provider.
This follows the principle of data minimization, which encourages systems to process and expose only the personal information necessary for a specific purpose.
It also aligns with Privacy by Design, where privacy is considered during architectural decisions rather than added later as an interface-level restriction.
This distinction matters because hiding a phone number visually is not the same as protecting it.
For example, removing a phone number from the delivery screen while continuing to return it in the API response does not solve the underlying problem. Anyone with access to the response could still retrieve it.
A stronger approach is to ensure that the client application never receives the number unless it has a legitimate reason to access it.
Of course, Masked Calling does not eliminate every privacy risk. The backend and telephony provider still process real phone numbers, and users may voluntarily disclose personal information during a conversation. The platform must therefore continue applying appropriate access controls, retention policies, and security measures.
Nevertheless, it significantly reduces an otherwise unnecessary exposure of personal contact information between users.
What about Masked Messaging?
The same approach can also be applied to text messages through a concept known as Masked Messaging. In delivery systems, couriers and customers may need to exchange messages to share instructions or clarify delivery details without exposing their personal phone numbers. Instead of allowing direct communication through SMS or WhatsApp, the platform can act as an intermediary, forwarding messages between participants or providing an in-app messaging feature. Similar to Masked Calling, communication can be restricted to the duration of an active delivery, preventing unwanted contact after its completion. This approach reinforces the same principle of Privacy by Design, allowing users to communicate when necessary while minimizing the exposure of their personal information.
Conclusion
A feature as simple as a Call Customer button can introduce privacy concerns that are easy to overlook during development.
In delivery systems, couriers and customers often need to communicate, but that does not mean they need permanent access to each other’s personal phone numbers.
Masked Calling offers an alternative by introducing a temporary communication layer that connects both participants without directly exposing their contact information.
From an architectural perspective, implementing this feature requires more than integrating a telephony API. It involves choosing how calls are initiated, ensuring accurate routing, managing temporary sessions and active calls independently, and coordinating business rules with an external provider.
These challenges are part of building a communication capability that respects both the delivery workflow and the privacy of its participants.
Ultimately, the most important principle is that systems should expose the capabilities users need without unnecessarily exposing the sensitive information behind those capabilities.
In this case, the courier needs a way to contact the customer, not access to the customer’s personal phone number.
