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; the lifecycle model this page references is on Proxy session model.1. Symptom map
Find your symptom, then jump to the section that works it:
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 returnssession_id, proxy_number, status, ttl_minutes, and expires_at; every later check reads those back:
session_id. A404 NOT_FOUNDhere 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.status.activeforwards;closedandexpireddo not. A session your order state believes is open but that readsclosedwas ended by an earlierDELETE— find that call in your logs. One that readsexpiredhit its TTL (section 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.expires_at. Compare it against now. Anactive-looking row pastexpires_atstill rejects traffic — expiry is enforced on access; the sweeper is the cleanup pass, not the gate (section 3).
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; the create and close flows are walked end to end in the masking sessions guide.
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_atpasses, even while the row still readsactive. A participant texting one minute pastexpires_atgets silence even though your list call still shows the session. If your “session died early” report has a timestamp just pastexpires_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 readsactivebut forwards nothing. Do not build on the intermediate status — treatexpires_atas 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
DELETEreturns409 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.
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 — 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.
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 and the webhook security page.
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:
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 and Troubleshooting: signature verification failures.
5. Sender-side mask leg failures: voice bag vs message
When a session readsactive, 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:
- Find the forwarded send in the Delivery log — the proxy-attributed message row.
- A row stuck at
sentorsubmitted_no_receiptis a receipt problem → Troubleshooting: message sent but no delivery receipt. - A terminal
undeliveredorfailedrow carries the carrier verdict → decode it on Troubleshooting: message undelivered or failed. - A send that never got past queueing → Troubleshooting: message queued.
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:
- Both directions fail. The inbound call to the proxy number is not resolving — confirm the caller is dialing the
proxy_numberfrom one of the two participant numbers (a third number never bridges, by design). - 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 and Troubleshooting: routing and channel misconfiguration.
- 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 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/measorganizationId. - 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, andcreated_atlet 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.forwardedwith 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.
See also
- Proxy session model — the lifecycle, forwarding resolution, and pool model this page reads back against
- Choosing a privacy primitive — the decision tree and TTL picker, including the error taxonomy in compact form
- Masking sessions guide — the end-to-end create → route → close loop this page debugs
- Number Masking — field-level reference for the session endpoints
- Webhook consumer guide — subscription registration and delivery semantics for the three proxy events
- Troubleshooting: message sent but no delivery receipt — the receipt-side sibling for the sender leg