Skip to main content

Mask two parties’ numbers with a proxy session

A masking session lets two people text and call each other without either one ever seeing the other’s real phone number. This guide walks a session through its full life — create, route SMS and voice through the proxy number, terminate, and verify — and ends with a worked rideshare example you can replay against your own order states. The endpoint surface is field-level at Number Masking and the Proxy API reference; this guide is the sequence.

1. What a masking session is

A session binds three numbers:
  • Party A — one participant’s real number, in E.164 (+14155550100).
  • Party B — the other participant’s real number.
  • The proxy number — an E.164 number Orbit draws from the shared number pool and holds for the session’s lifetime.
Party A sends to the proxy number; Orbit sees the sender matches the session and forwards to party B with the proxy number as sender. Party B replies the same way. Neither party ever sees participant_a or participant_b — the only address either side can reach or see is the proxy number. The lifecycle has three statuses: An expired session is a session you forgot, not a leak: the sweeper runs on schedule and hands the number back. Close explicitly when your app knows the moment of completion — a trip finished, a delivery confirmed — so the number frees up right away and neither party can recontact the other past the point your product intends.

2. Create a session

Post the two participants and, optionally, how long the session may stay open:
What to know when sizing the request:
  • Both participants are E.164 — leading +, country code, digits, nothing else. A malformed number returns 400 naming the field.
  • The two participants must differ. Identical numbers return 400.
  • ttl_minutes is the retention window: how long the proxy number stays bound to this pair. It defaults to 60, and accepts 1 up to 1440 (24 hours). Pick the longest window the two parties legitimately need to reach each other — a pickup, a dispatch, an appointment — not longer.
  • The proxy number comes from the shared pool, not from your organization’s one-time free trial number. There is no per-organization concurrency cap, so one session per active order is normal usage.
  • The expires_at timestamp is server-computed; store it if your product wants to warn participants before a session lapses.
Create and delete require an elevated workspace role (owner, admin, or developer) and are rate-limited to 20 requests per minute; list and get run at 60. Reuse is a mistake — never point a second order at a number from an earlier session. Create one session per order, ride, or appointment, and let the pool recycle.

3. Route SMS and voice through the proxy number

There is nothing to configure. The session routes on two checks, applied to every inbound SMS or call that lands on the proxy number:
  1. The proxy number resolves to an active, unexpired session.
  2. The sender matches participant_a or participant_b.
On a match, Orbit forwards the SMS body or the call leg to the other participant, sent from the proxy number. Voice calls between the participants bridge the same way — dial the proxy number, reach the counterparty. The forward runs through the same billed message path as any other send, so wallet balance and rate limits apply as they do elsewhere in your account; the proxy session id is attached to the message record so forwarded traffic is attributable. Three cases deliberately do not forward:
  • A third number texts or calls the proxy number. The sender matches neither participant, so the traffic is dropped and nothing is delivered. Only the two bound numbers can ever use the session.
  • The session is closed or expired. After termination the proxy number no longer resolves to anything, and inbound traffic to it goes nowhere.
  • The TTL has passed but the sweep hasn’t run yet. An active-looking row past its expires_at still rejects traffic — expiry is enforced on access, and the scheduled sweeper is the cleanup pass, not the gate.
Because only the proxy number is ever visible, also render it that way in your own UI: show both parties the proxy number as the conversation’s address, and neither participant collects a personal number from your product surface either.

4. Terminate the session and redact the pairing

End a session the moment your product knows the exchange is over:
Closing does three things in one step: the session flips to closed, the proxy number is released back to the pool for reassignment, and inbound traffic to that number stops resolving. The redaction is complete at close — neither party can reach the other afterward, and the number that linked them no longer names either of them. Keep the session’s proxy number in your conversation view after close, since it remains the only address either participant ever saw; pair it with message history redaction if your data policy also covers stored bodies. If you close nothing, the TTL is your retention bound: at expires_at the session stops accepting traffic, and the sweeper marks it expired and releases the number in the same pass. Edge rules worth encoding in your completion handler:
  • Only an active session closes. A DELETE on an already closed or expired session returns 409 — treat it as “already ended,” not as a failure to retry.
  • If your completion event fires near the TTL boundary, the sweeper may win the race and the close comes back 409. The outcome is identical — the number is released either way — so treat a 409 on close as a successful end state.
  • Reconcile with GET /api/v1/proxy/sessions?status=active: if your app believes an order is finished but a session still reads active, close it. That list call is the drift check between your order states and the proxy lifecycle.

5. Verify the flow from webhook events

Subscribe a webhook endpoint to the three proxy events and you can verify every step above without polling — see the webhook consumer guide for endpoint registration and signature verification. The events arrive in this order during a healthy session: 1. proxy.session.created — the session exists and its number is bound.
2. proxy.message.forwarded — one per forwarded message, telling you the direction:
3. proxy.session.closed — the pairing is released:
The verification sequence for a new integration:
  1. Create a session between two test numbers you control. Expect proxy.session.created and assert the proxy_number is not either participant.
  2. Text the proxy number from the participant A handset. Expect proxy.message.forwarded with from_participant equal to A and to_participant equal to B, and the message arriving on B from the proxy number.
  3. Reply from B. Expect the webhook with the direction reversed — confirmation the session is symmetric.
  4. Text the proxy number from a third number. Expect silence: no forward, no webhook.
  5. Delete the session. Expect proxy.session.closed, then text the proxy number once more from A — nothing should forward.
Pass all five and your masking flow is airtight in both directions.

Worked example: a delivery from dispatch to handoff

A courier app masks the courier–customer channel for one delivery. Dispatch assigns a courier. The app creates a session with a 90-minute TTL — dispatch through handoff, plus a buffer for gate and lobby calls:
proxy.session.created arrives; the app shows both sides the proxy number as the contact for this delivery. The customer texts the proxy number asking the courier to leave the package at the side door. The inbound resolves to the session, the sender matches party B, and the text reaches the courier from the proxy number — proxy.message.forwarded records the direction for the conversation view. The courier calls back the same number and the voice leg bridges to the customer. Handoff confirms. The app deletes the session. proxy.session.closed arrives, the number goes back to the pool, and neither side can recontact the other. If the confirmation event is ever lost, the 90-minute TTL closes the session on schedule through the sweeper instead — slower, same end state.

See Also