> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Proxy session (number masking) model

> How a masked proxy session works: two participants and one shared pool number, a TTL-bounded lifecycle with an explicit close, the forwarding resolution that keeps both real numbers private, and how sessions differ from short-links and inbound routing rules.

# Proxy session (number masking) model

A proxy session lets two people reach each other — by SMS or voice — without either one learning the other's real phone number. You create a session between two participant numbers, Orbit allocates one shared pool number to the session, and for the session's lifetime every message or call routed through that pool number is forwarded to the other participant. When the session ends, the pool number goes back into the shared pool and the link dies.

This page is the conceptual model: what a session is, how its lifecycle runs, how forwarding resolves the counterparty, how the shared pool stays fair, and how masking differs from short-links and inbound routing. For the full request flow and error handling, see the [number masking guide](/guides/number-masking-guide); for request and response schemas, the [proxy API reference](/api-reference/proxy); for how masking sits next to OTP and other privacy primitives, [Choosing a privacy primitive](/guides/number-masking-privacy-sessions).

## 1. What a proxy session is

A session is four things:

* **Two participants.** `participant_a` and `participant_b`, both E.164 numbers, and they must be different numbers. One is not special — the session is symmetric, and forwarding uses whichever participant sent the inbound traffic.
* **One shared pool number.** Orbit claims a number from the platform's shared proxy pool and binds it to the session for its lifetime. Both participants address the same number; it is the only number either side ever sees.
* **A TTL.** `ttl_minutes` bounds how long the binding lives — 1 to 1440 minutes (24 hours), defaulting to 60 when you omit it. The TTL is the session's disposal plan: pick the window in which the two parties legitimately need each other.
* **An id and an expiry.** The create response returns `session_id`, the allocated `proxy_number`, and `expires_at`, the moment the session stops forwarding.

Create one with `POST /api/v1/proxy/sessions` — body `{ participant_a, participant_b, ttl_minutes? }`. List sessions with an optional `status` filter on `GET /api/v1/proxy/sessions`, fetch one with `GET /api/v1/proxy/sessions/:id`, and end it with `DELETE /api/v1/proxy/sessions/:id`.

A session belongs to your tenant. Your tenant's sessions, participants, and audit entries are yours alone; the only thing shared across the platform is the pool the numbers come from.

## 2. Session lifecycle

```text theme={null}
POST /api/v1/proxy/sessions
          │
          ▼
   create  ──▶  active ──────────────┐
                │    │               │
                │    │               ▼
                │    │          DELETE /sessions/:id
                │    │          → closed (explicit)
                │    ▼
                │  TTL elapses → expired (sweeper)
                │    │
                ▼    ▼
          pool number released either way
```

A session is exactly one of three statuses: `active`, `closed`, or `expired`.

* **active** → **closed** when you call `DELETE /api/v1/proxy/sessions/:id`. This is the explicit ending.
* **active** → **expired** when the TTL lapses. A platform sweeper runs every five minutes, marks lapsed sessions `expired`, and releases their numbers. Because the sweep is periodic, an expired number can linger up to one sweep interval past `expires_at` before recycling.
* Forwarding also stops at the wall-clock expiry: inbound traffic that arrives past `expires_at` is not forwarded even before the sweeper gets to the row.

**Close explicitly when your app owns a completion event.** Trip ended, delivery confirmed, appointment concluded — the close releases the pool number immediately instead of waiting for the sweep, and it makes a finished transaction un-recontactable, which is the privacy property you bought masking for. Either path releases the number: a raced close that loses to the sweeper gets a 409, and that is a successful end state, not a failure.

**Let the sweeper handle silent endings.** A marketplace thread that goes quiet has no completion event. Size the TTL like the cleanup window it is, and let expiry do the work.

## 3. How forwarding works

Inbound SMS and calls to proxy numbers arrive on the platform's internal inbound webhook (`POST /api/v1/proxy/inbound`), an authenticated server-to-server path that is not callable by either participant. For each inbound message, the platform:

1. Resolves the session that owns the destination pool number.
2. Checks the session has not expired.
3. Matches the sender to one of the two participants — a sender that matches neither participant is never forwarded.
4. Sends the message to the other participant with the pool number as the sender.

That last step is the privacy boundary. The forwarded SMS or voice leg presents the pool number as its origin, so the receiving participant never sees a real counterpart number — and the original sender never sees theirs delivered. Each side only ever holds the shared pool number.

```text theme={null}
Rider's phone                Platform (proxy)            Driver's phone
      │                             │                           │
      │  SMS to pool number         │                           │
      │ ──────────────────────────▶ │                           │
      │                             │ 1. resolve session        │
      │                             │ 2. sender = participant_a │
      │                             │ 3. forward to             │
      │                             │    participant_b,         │
      │                             │    from = pool number     │
      │        forwarded message    │                           │
      │ ◀────────────────────────── │ ─────────────────────────▶│
      │        (from pool number)   │                           │
      │                             │                           │
```

Either direction works: forwarding is symmetric because the sender match picks the counterparty. In ride-share terms the rider and driver substitute freely; the model is the same in the other direction.

Sequence of operations for one inbound SMS (`rider → proxy → driver`):

```text theme={null}
sequenceDiagram
    participant Rider as Rider
    participant Proxy as Proxy session
    participant Driver as Driver
    Rider->>Proxy: SMS to pool number
    Proxy->>Proxy: resolve session, match sender
    Proxy->>Driver: forward (from = pool number)
```

## 4. Pool fairness

Sessions draw from a shared platform pool, and there is no per-organization concurrency cap on active sessions. A marketplace, delivery, or ride-share tenant can hold as many sessions as its transaction volume needs; one organization's load never blocks another's claim.

Fairness instead comes from recycling:

* On close or expiry the number is released and becomes claimable again by any organization. Numbers churn through the platform rather than accumulating in one tenant.
* A released number is re-issued only after release completes, so a stale binding can never route traffic to the wrong session — closed-session inbound traffic short-circuits on the session lookup.
* Releases are ownership-scoped: only the organization that claimed a number can release it, so a late or duplicated close cannot free a number that has since been re-claimed by another session.

Practical consequence: you do not need to pool-manage. You create a session per transaction, close on completion, and trust the pool — the one discipline that still matters is not hoarding TTLs longer than the transaction needs.

## 5. Validation and error shapes

The session endpoints fail in a small, deliberate set of ways:

| Trigger                                                  | Response               | Meaning                                                    |
| -------------------------------------------------------- | ---------------------- | ---------------------------------------------------------- |
| `participant_a` equals `participant_b`                   | `400 VALIDATION_ERROR` | The two sides must be different numbers                    |
| `ttl_minutes` outside 1–1440, or a non-E.164 participant | `400` validation error | Bounds are enforced at creation                            |
| Close (`DELETE`) on a `closed` or `expired` session      | `409 CONFLICT`         | The session already ended — treat as success, do not retry |
| Fetch or close a session id that does not exist          | `404 NOT_FOUND`        | Unknown session                                            |

Two of these encode races, not bugs. A 409 on close means the sweeper or an earlier close got there first — the desired end state holds either way, so log it and move on; retrying or alerting on it creates noise about a correct outcome.

## 6. What masking is — and is not

Masking is easy to confuse with two neighboring features. The distinction matters because they solve different problems:

* **Against short-links and landing pages.** A short-link intentionally exposes intent: the recipient clicks, and the click is the signal — tracked, attributed, led back to a landing page. Masking does the opposite with identity: it hides *who* is on each side while contact happens. Links reveal a call to action; a proxy session hides the parties.
* **Against inbound message routing.** Routing rules are tenant-level and durable: they decide, for every inbound message on your numbers, which destination handles it — webhook, inbox, queue, auto-reply. Masking is none of that. It is a per-session overlay for exactly one pair of participants, with no match rules and no routing targets; it lives for its TTL and then disappears. Routing answers "where do my inbound messages go"; a proxy session answers "let these two specific people talk, privately, for a while."

Put them together and the boundaries compose: your routing rules still govern ordinary inbound traffic on your numbers, your links still carry campaigns, and a proxy session overlays the one transaction where two parties must not exchange numbers.
