> ## 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.

# Troubleshooting: number masking and proxy-session failures

> Diagnose a masked proxy session that will not bind, forwards inbound-only, bridges the wrong call leg, or drifts on expiry — read back the session object, decode the TTL race, fix the missing webhook subscription, separate voice-leg failures from message-leg failures, and bundle the right evidence before contacting support.

# Troubleshooting: number masking and proxy-session failures

A masked proxy session fails in a small, predictable set of ways: it never binds, it forwards in one direction only, a voice leg bridges to the wrong party, or it ends earlier or later than your order state expects. This page is the playbook for each of those — what to read back, what each symptom means, and what to hand support when the symptom is on the platform side. If you are still deciding whether masking is the right primitive, start with [Choosing a privacy primitive](/guides/number-masking-guide); the lifecycle model this page references is on [Proxy session model](/concepts/proxy-session-model).

## 1. Symptom map

Find your symptom, then jump to the section that works it:

| Symptom                                                 | Most likely cause                                                                                                     | Section |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------- |
| Create returns `400` and nothing binds                  | Two identical participants, a non-E.164 number, or `ttl_minutes` outside 1–1440                                       | 2       |
| Session binds but no SMS ever forwards                  | Destination or sender does not resolve to an active session, or the sender matches neither participant                | 2, 4    |
| Inbound forwarding works, outbound replies vanish       | The reply is not addressed to the proxy number, or the sender side of the leg is failing independently of the session | 5       |
| Voice calls do not bridge, SMS does                     | The voice leg is a separate failure class — bridged through the platform's voice path, not the SMS path               | 5       |
| Session ends before the TTL you set, or lingers after   | Expiry drift: wall-clock expiry, the five-minute sweeper, or a 409-losing close race                                  | 3       |
| No lifecycle visibility at all                          | The webhook subscription is missing `proxy.message.forwarded` or the endpoint was never subscribed                    | 4       |
| Your app thinks a session is open but the API disagrees | Order-state drift — reconcile against the list call                                                                   | 2       |

Two errors are deliberately **not** failures: `409 CONFLICT` on close means the session already ended (section 3), and silence when a third number texts the proxy number is the privacy boundary working as designed — a sender matching neither participant is dropped, not forwarded.

## 2. Read back the session object

Every diagnosis starts the same way: fetch the session and compare what the API says with what your order state believes. Create returns `session_id`, `proxy_number`, `status`, `ttl_minutes`, and `expires_at`; every later check reads those back:

```bash theme={null}
# Fetch the one session you suspect
curl "https://api.orbit.devotel.io/api/v1/proxy/sessions/proxySession_3f8d2a91c4" \
  -H "X-API-Key: dv_live_sk_..."

# Or list everything still open — the order-state drift check
curl "https://api.orbit.devotel.io/api/v1/proxy/sessions?status=active" \
  -H "X-API-Key: dv_live_sk_..."
```

```json theme={null}
{
  "data": {
    "session_id": "proxySession_3f8d2a91c4",
    "proxy_number": "+14155555678",
    "participant_a": "+14155550100",
    "participant_b": "+14155550399",
    "status": "active",
    "ttl_minutes": 45,
    "expires_at": "2026-08-27T12:45:00.000Z",
    "created_at": "2026-08-27T12:00:00.000Z"
  }
}
```

Read the four fields in order and the symptom usually decodes itself:

1. **`session_id`.** A `404 NOT_FOUND` here means the id does not exist in your organization — you are holding a stale or mistyped id. Fetch it from the create response on your order record; list sessions to reconcile.
2. **`status`.** `active` forwards; `closed` and `expired` do not. A session your order state believes is open but that reads `closed` was ended by an earlier `DELETE` — find that call in your logs. One that reads `expired` hit its TTL (section 3).
3. **`proxy_number`.** Confirm both participants are actually addressing this number. A participant texting or calling any other number — a real participant number, an old session's proxy number — never resolves to this session. The proxy number is the only address the session owns.
4. **`expires_at`.** Compare it against now. An `active`-looking row past `expires_at` still rejects traffic — expiry is enforced on access; the sweeper is the cleanup pass, not the gate (section 3).

Three create-time rules keep a session from ever binding, and all return `400 VALIDATION_ERROR`: `participant_a` equals `participant_b`, a participant is not E.164 (leading `+`, country code, digits, nothing else), or `ttl_minutes` is below 1 or above 1440. These are request errors — they never succeed on retry without a change.

The full state machine this read-back walks is section 2 of [Proxy session model](/concepts/proxy-session-model); the create and close flows are walked end to end in the [masking sessions guide](/guides/number-masking-privacy-sessions).

## 3. TTL and expiry drift

"Drift" here means the session ends on a different schedule than your order state expects. Three mechanics explain every case:

* **Expiry is wall-clock, enforced on access.** Forwarding stops the moment `expires_at` passes, even while the row still reads `active`. A participant texting one minute past `expires_at` gets silence even though your list call still shows the session. If your "session died early" report has a timestamp just past `expires_at`, this is the explanation — not a fault.
* **The sweeper is periodic, not instant.** A platform job runs every five minutes, marks lapsed sessions `expired`, and releases their numbers. Between wall-clock expiry and the sweep, the row sits in a limbo that reads `active` but forwards nothing. Do not build on the intermediate status — treat `expires_at` as the truth and the status as bookkeeping that catches up within one sweep interval.
* **A 409 close race is a successful end state.** If your completion handler closes near the TTL, the sweeper may expire the session first, and your `DELETE` returns `409 CONFLICT`. Both paths release the number. Handle a 409 on close as "already ended" — log it and move on; alerting or retrying on it creates noise about a correct outcome.

If sessions end **earlier** than intended, the fix is sizing, not platform behavior: `ttl_minutes` is bounded at 1440 and defaults to 60 when omitted. An omitted TTL on a use case that legitimately spans hours ends at the one-hour mark by design. Set the TTL to the longest window the two parties need each other — the per-use-case table is section 3 of [Choosing a privacy primitive](/guides/number-masking-guide) — and create a fresh session when a marketplace thread re-opens, rather than stretching one TTL as insurance.

If sessions **linger** past what your order state expects, that is the drift the list call catches: `GET /api/v1/proxy/sessions?status=active` against your open orders, and close anything your app believes is finished. Your completion handler owning the close is the primary path; the TTL is the backstop, not the plan.

## 4. Missing webhook subscription on the inbound leg

A session can forward perfectly while your integration sees nothing, because the lifecycle events were never subscribed. Three events exist — and each answers a different operational question:

* `proxy.session.created` — the session exists and the number is bound.
* `proxy.message.forwarded` — one per forwarded message, telling you the direction (`from_participant`, `to_participant`, `channel`).
* `proxy.session.closed` — the pairing is released.

The common miss is subscribing to `created` and `closed` only. That pair gives you lifecycle bookkeeping and leaves the conversation itself invisible: without `proxy.message.forwarded` you cannot render a two-sided thread or reconcile message flow, because it is the only event that says which direction each message moved. Subscribe to all three; registration, retries, and signature verification are in the [webhook consumer guide](/guides/webhook-consumer) and the [webhook security page](/webhooks/security).

When you suspect a forwarding gap, compare two clocks: the message actually arriving on the counterparty handset, and your webhook log. A delivered message with no `proxy.message.forwarded` event is a subscription or ingestion gap on your side. An event with no delivered message is a sender-side leg failure — section 5.

A healthy `proxy.message.forwarded` delivery looks like this:

```json theme={null}
{
  "session_id": "proxySession_3f8d2a91c4",
  "proxy_number": "+14155555678",
  "from_participant": "+14155550100",
  "to_participant": "+14155550399",
  "channel": "sms"
}
```

The corresponding **failure** shape is the absence of this event paired with an inbound SMS you know arrived on the proxy number — that pairing, inbound present / event absent, is the signature to capture for support (section 6). Note the inverse discrimination too: a first-direction `proxy.message.forwarded` followed by silence in the reverse direction is not a webhook gap — the reply leg itself is failing, so work section 5.

Webhook deliveries are at-least-once and signed; dedupe by the event id and verify the HMAC before acting, exactly as for message status events. The adjacent discipline is on [Troubleshooting: webhook event dedup](/troubleshooting/webhook-event-dedup) and [Troubleshooting: signature verification failures](/webhooks/troubleshooting-signature-failures).

## 5. Sender-side mask leg failures: voice bag vs message

When a session reads `active`, is within TTL, and a participant's inbound traffic still does not reach the counterparty, the session layer has already done its job — the failure is on the sender side of the leg Orbit placed to the other participant. Split the diagnosis by channel, because the two legs fail differently:

**Message leg (SMS).** The forwarded SMS runs through the same billed message path as any other send: wallet balance and rate limits apply, the proxy session id is attached to the message record, and the send produces the same lifecycle receipts and failures as an ordinary outbound message. Work it exactly like one:

1. Find the forwarded send in the [Delivery log](/guides/delivery-log) — the proxy-attributed message row.
2. A row stuck at `sent` or `submitted_no_receipt` is a receipt problem → [Troubleshooting: message sent but no delivery receipt](/troubleshooting/submitted-no-receipt).
3. A terminal `undelivered` or `failed` row carries the carrier verdict → decode it on [Troubleshooting: message undelivered or failed](/troubleshooting/message-undelivered-failed).
4. A send that never got past queueing → [Troubleshooting: message queued](/troubleshooting/message-queued).

One sender-side shape is unique to masking: to inject a reply from your own service — a dispatcher answering, a completion notice — you send an outbound SMS **to the proxy number**, never to either participant's real number. The session match keys off the destination. A reply addressed to a participant number bypasses the session entirely: it either delivers unmasked (a privacy leak you built yourself) or, more often, simply fails to thread. Address replies to the `proxy_number` you stored at create.

**Voice leg (the voice bag).** A voice call between participants is a separate bridge: the participant dials the proxy number, and the platform bridges the call to the counterparty, presenting the proxy number. SMS forwarding succeeding while voice fails is the tell that these are independent legs — do not debug the session when only one leg is down. Work the voice side in order:

1. **Both directions fail.** The inbound call to the proxy number is not resolving — confirm the caller is dialing the `proxy_number` from one of the two participant numbers (a third number never bridges, by design).
2. **One direction fails.** The outbound leg to the counterparty is a call-leg failure — decode it like any bridged-call failure on [Troubleshooting: voice call quality](/troubleshooting/voice-call-quality) and [Troubleshooting: routing and channel misconfiguration](/troubleshooting/routing-and-channel-misconfig).
3. **Calls blocked only during certain hours.** If the participants are in the US, a federal dialing window applies to outbound voice legs and has no opt-out — see [Troubleshooting: TCPA window blocked calls](/troubleshooting/tcpa-window-blocked-calls) before treating it as a session fault.

## 6. What to bundle before contacting support

Gather this before you open a ticket — it is the difference between a one-reply resolution and a diagnosis loop:

* **Your tenant ID** — shown in the dashboard under Settings → Organization, and returned by `GET /api/v1/me` as `organizationId`.
* **The session id** (`proxySession_…`) and the **proxy number** the session held.
* **The full session object** from `GET /api/v1/proxy/sessions/:id` — status, `expires_at`, and `created_at` let support place the failure against the lifecycle without a back-and-forth.
* **Timestamps with direction.** "Participant A texted the proxy number at 12:31Z, nothing forwarded" beats "messages not working." For a voice leg, note which direction failed.
* **The webhook evidence.** A captured failing-or-missing delivery: your endpoint's logged request for `proxy.message.forwarded` with its signature headers, or the observed absence of that event alongside a delivered inbound message. Include the event id if a delivery did arrive — duplicate deliveries are expected under at-least-once semantics.
* **The forwarded message id** (`msg_…`) from the Delivery log when the sender-side SMS leg is the failing part, or the **call id** when it is the voice leg.

A wide signal changes the escalation shape: one stuck session is a per-session fault; many sessions failing to forward at the same time, across participants and destinations, is a platform-side signal — say so explicitly in the ticket and lead with the timestamps.

## See also

* [Proxy session model](/concepts/proxy-session-model) — the lifecycle, forwarding resolution, and pool model this page reads back against
* [Choosing a privacy primitive](/guides/number-masking-guide) — the decision tree and TTL picker, including the error taxonomy in compact form
* [Masking sessions guide](/guides/number-masking-privacy-sessions) — the end-to-end create → route → close loop this page debugs
* [Number Masking](/numbers/masking) — field-level reference for the session endpoints
* [Webhook consumer guide](/guides/webhook-consumer) — subscription registration and delivery semantics for the three proxy events
* [Troubleshooting: message sent but no delivery receipt](/troubleshooting/submitted-no-receipt) — the receipt-side sibling for the sender leg
