# How to Build Two-Way Number Masking with the Session API (2026)

> A developer guide to session-based number masking: bind two numbers to one virtual number with a single API call, route calls dynamically from your own server, and receive lifecycle events. Code samples included.

Source: https://edesy.in/blog/number-masking-session-api-build-guide-2026

If you run a marketplace, a delivery or ride service, or any product where two strangers need to talk repeatedly around an order, one-shot "call now" bridging isn't quite enough. You want a **single virtual number that both people can call, in either direction, for as long as the order is live** — and that stops working the moment it's done.

That's exactly what the **Session API** does. This guide shows you how to build two-way number masking end to end: create a session, handle inbound calls, route them dynamically from your own server, and react to lifecycle events.

> New to the concept? Start with [What Is Number Masking?](https://edesy.in/blog/what-is-number-masking-call-masking-guide-2026) and the [Number Masking product overview](https://edesy.in/number-masking). For the full reference, see the [Two-Way Sessions docs](https://edesy.in/docs/number-masking/sessions).

## Sessions vs. click-to-call

There are two ways to mask a call, and they suit different moments:

- **Click-to-call** (`POST /masking/calls`) is *outbound*: the platform dials Party A, then bridges Party B. Perfect for a "Call the driver" button that places one call right now.
- **Sessions** (`POST /masking/sessions`) are *inbound*: you get a virtual number back, and *either* party dials it whenever they want, as many times as you allow, until the session expires.

For an ongoing buyer↔seller or customer↔driver relationship, sessions are the right primitive. The rest of this guide focuses on them. (See the [Session API feature page](https://edesy.in/number-masking/features/session-api) for a summary.)

## Step 1: Create a session

When an order is created, bind the two numbers and hand the virtual number to both parties. Pass your own order id as `reference` so the call is idempotent, and an `expiry_minutes` so the number frees itself when the order is done.

```bash
curl -X POST https://voice-api.edesy.in/v1/masking/sessions \
  -H "Authorization: Bearer vp_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "party_a": "9876543210",
    "party_b": "9123456789",
    "reference": "ORD-4821",
    "expiry_minutes": 120
  }'
```

```javascript
const res = await fetch("https://voice-api.edesy.in/v1/masking/sessions", {
  method: "POST",
  headers: {
    Authorization: "Bearer vp_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    party_a: buyer.phone,
    party_b: seller.phone,
    reference: order.id,      // idempotent: safe to retry
    expiry_minutes: 120,
  }),
});
const { data } = await res.json();
// Give data.virtual_number to both buyer and seller
```

The response contains the `virtual_number` and a `session_id`:

```json
{
  "data": {
    "session_id": "3f9a1c72-5b8e-4d21-9c34-7a2e6f0b1d55",
    "virtual_number": "918071387146",
    "party_a": "9876543210",
    "party_b": "9123456789",
    "reference": "ORD-4821",
    "status": "active",
    "expires_at": "2026-07-14T18:30:00Z",
    "total_calls": 0,
    "created_at": "2026-07-14T18:00:00Z"
  }
}
```

Because create is **idempotent on `reference`**, retrying the same order id returns the same session — no duplicate numbers, no bookkeeping on your side. If every enrolled number is busy for these parties, you get a `422` telling you to [enroll more numbers](https://edesy.in/docs/number-masking/sessions/enroll-numbers) — your signal to add capacity.

## Step 2: Let either party call

Now both people simply call the `virtual_number`. The platform looks at who is calling and bridges them to the other party; both see the virtual number as caller ID. There's nothing more to build for the happy path — routing is automatic.

One virtual number [safely serves many concurrent sessions](https://edesy.in/number-masking/features/number-pooling) at once (as long as no party is live on it twice), so you don't need one number per order.

## Step 3: Manage the session as the order changes

Orders get delayed, extended, or cancelled. Reflect that on the session:

```bash
# Order extended — push the expiry out
curl -X PATCH https://voice-api.edesy.in/v1/masking/sessions/SESSION_ID \
  -H "Authorization: Bearer vp_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "expiry_minutes": 120 }'

# Temporarily block calls (e.g. dispute on hold)
curl -X PATCH https://voice-api.edesy.in/v1/masking/sessions/SESSION_ID \
  -H "Authorization: Bearer vp_YOUR_API_KEY" \
  -d '{ "status": "paused" }'

# Order done — end it now and free the number
curl -X DELETE https://voice-api.edesy.in/v1/masking/sessions/SESSION_ID \
  -H "Authorization: Bearer vp_YOUR_API_KEY"
```

If you set an `expiry_minutes`, you don't even need the `DELETE` — the session expires on its own and the number returns to the pool. See [Manage a Session](https://edesy.in/docs/number-masking/sessions/manage-session) for the full surface.

## Step 4 (optional): Route calls dynamically from your own server

The default routing — connect the two parties on the session — covers most cases. But sometimes you want to decide the target **per call, at call time**: round-robin across a team, route by time of day, or block a caller you've flagged.

For that, point a number at your own HTTPS endpoint with the **[dynamic routing webhook](https://edesy.in/docs/number-masking/sessions/dynamic-routing)**. When a call arrives, the platform sends you a signed request:

```json
{
  "event": "masking.route",
  "call_sid": "d28be7cd-9840-4b23-9627-199f1ef81fc6",
  "caller": "9876543210",
  "masked_number": "918071387146",
  "direction": "inbound",
  "timestamp": "2026-07-14T18:00:00Z"
}
```

Your endpoint replies with the number to connect to — or rejects the call:

```javascript
// Verify X-Edesy-Signature (HMAC-SHA256, "sha256=<hex>") over the raw body first
app.post("/edesy/route", (req, res) => {
  const agent = pickAvailableAgent();           // your logic
  if (!agent) {
    return res.json({ action: "reject", reject_reason: "All agents are busy. Please try later." });
  }
  res.json({ action: "connect", target_number: agent.phone });
});
```

You have a **5-second** budget to respond (the caller is on the line), and you can optionally fall back to the number's internal mapping if your endpoint is down. Every request is HMAC-signed so you can verify it's genuinely from us.

## Step 5: React to lifecycle events

Register one [event webhook](https://edesy.in/docs/number-masking/sessions/event-webhooks) and the platform will POST you `session.created`, `call.incoming`, `call.connected`, `call.ended`, `call.missed`, and `session.expired` — each signed with HMAC-SHA256. Use them to log connected calls against the order, flag missed calls for follow-up, or reconcile usage:

```json
{
  "event": "call.ended",
  "reference": "ORD-4821",
  "virtual_number": "918071387146",
  "caller": "9876543210",
  "callee": "9123456789",
  "direction": "a_to_b",
  "call_sid": "d28be7cd-9840-4b23-9627-199f1ef81fc6",
  "duration_sec": 92,
  "status": "completed",
  "timestamp": "2026-07-14T18:05:00Z"
}
```

Because `reference` is echoed on every event, you can tie each call straight back to the order that created it. A delivery log and a "send test event" endpoint are there for debugging.

## A note on scope

Edesy number masking is **voice-only** and uses **10-digit Indian mobile numbers**, billed at **Rs 1.50 per masked minute** on a prepaid wallet with a GST invoice — you pay only for connected minutes, with no per-seat licensing. There's no SMS masking.

## Putting it together

1. **Order created** → `POST /masking/sessions` with your order id as `reference`; hand `virtual_number` to both parties.
2. **They call** → the platform bridges them automatically (or asks your routing webhook).
3. **Order changes** → `PATCH` to extend, pause, or resume.
4. **Order done** → let it expire, or `DELETE` to free the number now.
5. **Throughout** → event webhooks keep your systems in sync.

That's a complete, privacy-safe, two-way calling channel scoped to the life of an order — in five API calls.

> Ready to build? Read the [Two-Way Sessions reference](https://edesy.in/docs/number-masking/sessions), grab an API key in the [portal](https://edesy.in/number-masking), and see [pricing](https://edesy.in/number-masking/pricing).

## Related reading

- [One-way vs two-way number masking](https://edesy.in/blog/one-way-vs-two-way-number-masking-2026)
- [How many masked numbers do you need](https://edesy.in/blog/how-many-masked-numbers-do-you-need-2026)
- [Number masking for marketplaces and gig platforms](https://edesy.in/blog/number-masking-for-marketplaces-gig-platforms-2026)
- [Number masking for delivery and logistics fleets](https://edesy.in/blog/number-masking-delivery-logistics-fleets-2026)
- [What number masking costs in India](https://edesy.in/blog/what-number-masking-costs-india-2026)

## Frequently Asked Questions

### What is session-based number masking?

Session-based number masking binds two real phone numbers to a single virtual number for a limited lifetime. Either party can call the virtual number and is bridged to the other, with routing decided by who is calling. When the order or ride ends, the session expires and the number is freed for reuse. It is the two-way alternative to one-shot click-to-call bridging.

### How do I connect two numbers with the number masking API?

Send a POST request to /masking/sessions with party_a, party_b, and an optional reference and expiry_minutes. The API returns a virtual_number that you give to both parties. Either party can then call that number until the session expires or you end it.

### Is the create-session call idempotent?

Yes. Create is idempotent on your reference id (an order, ride, or ticket id). Repeating the call for an active reference returns the existing session instead of allocating a new number, so retries and duplicate events are safe.

### Can I decide who a masked number connects to at call time?

Yes, with the dynamic routing webhook. Point a masked number at your HTTPS endpoint, and when a call arrives the platform sends you a signed masking.route request; your endpoint replies with the number to connect to, or rejects the call. This is useful for round-robin agent routing, time-based routing, and blocking abuse.

### Does number masking support SMS?

No. Edesy number masking is voice-only today — it masks phone calls between two parties. Numbers are 10-digit Indian mobiles, billed at Rs 1.50 per masked minute on a prepaid wallet.
